Инициализация переменных


В конструктор класса вставьте код установки начальных значений переменных, с помощью которых пользователь сможет управлять сценой Open GL:
COpenGL: : COpenGL()
{
//====== Контекст передачи пока отсутствует
m_hRC = 0;
//====== Начальный разворот изображения
m_AngleX = 35. f;
m_AngleY = 20. f;
//====== Угол зрения для матрицы проекции
m_AngleView = 45. f;
//====== Начальный цвет фона
m_clrFillColor = RGB (255,245,255);
//====== Начальный режим заполнения
//====== внутренних точек полигона
m_FillMode = GL_FILL;
//====== Подготовка графика по умолчанию
DefaultGraphic ();
//=== Начальное смещение относительно центра сцены
//=== Сдвиг назад на полуторный размер объекта
m_zTrans = -1.5f*m_fRangeX;
m_xTrans = m_yTrans = 0.f ;
// Начальные значения квантов смещения (для анимации)
m_dx = m_dy = 0.f;
//=== Мыть не захвачена
m_bCaptured = false;
//=== Правая кнопка не была нажата
m_bRightButton = false;
//=== Рисуем четырехугольниками m_bQuad = true;
//====== Начальный значения параметров освещения
m_LightParam[OJ = 50; // X position
m_LightParam[l] = 80; // Y position
m_LightParam[2] = 100; // Z position
m_LightParam[3] = 15; // Ambient light
m_LightPararn[4] = 70; // Diffuse light
m_LightParam[5] = 100; // Specular light
m_LightParam[6] = 100; // Ambient material
m_LightParam[7] = 100; // Diffuse material
m_LightParam[8] = 40; // Specular material
m_LightParam[9] = 70; // Shininess material
m_LightParam[10] = 0; // Emission material
}
Функция перерисовки
Перерисовка изображения OpenGL состоит в том, что обнуляется буфер цвета и буфер глубины — буфер третьей координаты. Затем в матрицу моделирования (GL_MODELVIEW), которая уже выбрана в качестве текущей, загружается единичная матрица (glLoadldentity). После этого происходит установка освещения, с тем чтобы на него не действовали преобразования сдвига и вращения. Лишь после этого матрица моделирования домножается на матрицу трансляции и матрицу вращений. Чтобы рассмотреть изображение, достаточно иметь возможность вращать его вокруг двух осей (X и Y). Поэтому мы домножаем матрицу моделирования на две матрицы вращений (glRotatef). Сначала вращаем вокруг оси X, затем вокруг оси Y:
HRESULT COpenGL: :OnDraw (ATL_DRAWINFO& di)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glLoadldentity{);
//====== Установка параметров освещения
SetLight ();
//====== Формирование матрицы моделирования
glTranslatef(m_xTrans,m_yTrans,m_zTrans);
glRotatef (m_AngleX, l.0f, 0.0f, 0.0f );
glRotatef (m_AngleY, 0.0f, l.0f, 0.0f );
//====== Вызов рисующих команд из списка
glCallList(1);
//====== Переключение буферов
SwapBuffers(m_hdc);
return S_OK;
}

Управление цветом фона


Возможность изменять цвет фона окна OpenGL удобно реализовать с помощью отдельного метода класса:
void COpenGL::SetBkColor()
{
//====== Расщепление цвета на три компонента
GLclampf red = GetRValue(m_clrFillColor)/255 . f,
green = GetGValue(m_clrFillColor)/255.f,
blue = GetBValue(m_clrFillColor)/255.f;
//====== Установка цвета фона (стирания) окна
glClearColor (red, green, blue, O.f);
//====== Непосредственное стирание
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
Вызов этого метода должен происходить при первоначальном создании окна, то есть внутри OnCreate, и при каждом изменении стандартного свойства (stock property) в окне свойств. Первое действие мы уже выполнили, а второе необходимо сделать, изменив тело функции OnFillColorChanged:
void COpenGL::OnFillColorChanged()
{
//====== Если выбран системный цвет,
if (m_clrFillColor & 0x80000000)
//====== то выбираем его по индексу
m_clrFillColor = GetSysColor(m_clrFillColor & Oxlf);
//====== Изменяем цвет фона окна OpenGL
SetBkColor ();
}
  
Подготовка сцены OpenGL
Считая, что данные о координатах точек изображаемой поверхности уже известны и расположены в контейнере m_cPoints, напишем коды функции DrawScene, которая создает изображение поверхности и запоминает его в виде списка команд OpenGL. Как вы помните, одним из технологических приемов OpenGL, которые ускоряют процесс передачи (rendering), является предварительная заготовка изображения, то есть запоминание и компиляция списка рисующих команд.
Напомним, что отображаемый график представляет собой криволинейную поверхность (например, равного уровня температуры). Ось Y, по которой откладываются интересующие пользователя значения функции, направлена вверх. Ось X направлена вправо, а ось Z — вглубь экрана. Часть плоскости (X, Z), для точек которой известны значения Y, представляет собой координатную сетку. Изображаемая поверхность расположена над плоскостью (X, Z), а точнее, над этой сеткой. Поверхность можно представить себе в виде одеяла, сшитого из множества лоскутов. Каждый лоскут мы будем задавать в виде четырехугольника, как-то ориентированного в пространстве. Все множество четырехугольников поверхности также образует сетку. Для задания последовательности четырехугольников в OpenGL существует пара команд:
glBegin (GL_QUADS) ;
// Здесь располагаются команды, задающие четырехугольники
glEnd() ;
Четырехугольник задается координатами своих вершин. При задании координат какой-либо вершины, например, командой givertex3f (х, у, z);, можно сразу же определить ее цвет, например, командой gicolor3f (red, green, blue);. Если цвета вершин будут разными, а режим заполнения равен константе GL_FILL, то цвета внутренних точек четырехугольника примут промежуточное значение. Конвейер OpenGL производит аппроксимацию цвета так, что при перемещении от одной вершины к другой он изменяется плавно.
Режим растеризации или заполнения промежуточных точек графического примитива задается командой glPolygonMode. OpenGL различает фронтальные (front-facing polygons), обратные (back-facing polygons) и двухсторонние многоугольники. Режим заполнения их отличается, поэтому первый параметр функции glPolygonMode должен определить тип полигона (GL_FRONT, GL_BACK или GL_FRONT_AND_BACK).
Второй параметр собственно и определяет режим заполнения. Он может принимать значение GL_POINT, GL_LINE или GL_FILL. Первый выбор даст лишь обозначение примитива в виде его вершин, второй — даст некий скелет, вершины будут соединены линиями, а третий заполнит все промежуточные точки примитива. По умолчанию принят режим GL_FILL и мы получаем сплошной лоскут.'Если в качестве первого параметра задать GL_FRONT_AND_BACK, то изменения второго параметра будут касаться обеих поверхностей одеяла. Другие сочетания дают на первый взгляд странные эффекты: так, если задать сочетание (GL_FRONT, GL_LINE), то лицевая сторона одеяла будет обозначена каркасом (frame view), а изнаночная по умолчанию будет сплошной (GL_FILL). Поверхность при этом будет полупрозрачна.
Мы решили оставить неизменным значение GL_FRONT_AND_BACK для первого параметра и дать пользователю возможность изменять режим заполнения (второй параметр glPolygonMode) по его желанию. Впоследствии внесем эту настройку в диалог свойств СОМ-объекта, а результат выбора пользователя будем хранить в переменной m_FillMode. С учетом сказанного введите коды реализации функции DrawScenel
//====== Подготовка изображения
void COpenGL::DrawScene()
{
//====== Создание списка рисующих команд
glNewListd, GL_COMPILE) ;
//====== Установка режима заполнения
//====== внутренних точек полигонов
glPolygonMode(GL_FRONT_AND_BACK, m_FillMode);
//====== Размеры изображаемого объекта
UINTnx = m_xSize-l, nz = m_zSize-l;
//====== Выбор способа создания полигонов
if (m_bQuad)
glBegin (GL QUADS);
//=== Цикл прохода по слоям изображения (ось Z) for (UINT z=0, i=0; z<nz; z++, i++)
//=== Связанные полигоны начинаются
//=== на каждой полосе вновь if (!m_bQuad)
glBegin(GL_QUAD_STRIP) ;
//=== Цикл прохода вдоль оси X
for (UINT x=0; x<nx; х++, i++)
{
// i, j, k, n — 4 индекса вершин примитива при
// обходе в направлении против часовой стрелки
int j = i + m_xSize,
// Индекс узла с большим Z
k = j+1, // Индекс узла по диагонали
n = i+1; // Индекс узла справа
// Выбор координат 4-х вершин из контейнера
float
xi = m_cPoints [i] . х,
yi = m_cPoints [i] .y,
zi = m_cPoints [i] . z,
xj = m_cPoints [ j ] .x,
yj = m_cPoints [ j ] .y,
zj = m_cPoints [ j ] .z,
xk = m_cPoints [k] .x,
yk = m_cPoints [k] . y,
zk = m_cPoints [k] . z,
xn = m_cPoints [n] .x,
yn = m_cPoints [n] .y,
zn = m_cPoints [n] . z,
//=== Координаты векторов боковых сторон
ах = xi-xn,
ay = yi-yn,
by = yj-yi,
bz = zj-zi,
//=== Вычисление вектора нормали
vx = ay*bz,
vy = -bz*ax,
vz = ax*by,
//=== Модуль нормали
v = float (sqrt (vx*vx + vy*vy + vz*vz) ) ;
//====== Нормировка вектора нормали
vx /= v;
vy /= v;
vz /= v;
//====== Задание вектора нормали
glNormalSf (vx,vyfvz);
// Ветвь создания несвязанных четырехугольников
if (m_bQuad)
{
//====== Обход вершин осуществляется
//=== в направлении против часовой стрелки
glColorSf (0.2f, 0.8f, l.f);
glVertex3f (xi, yi, zi);
glColor3f <0.6f, 0.7f, l.f);
glVertexSf (xj, уj, zj);
glColorSf (0.7f, 0.9f, l.f);
glVertexSf (xk, yk, zk);
glColorSf (0.7f, 0.8f, l.f);
glVertexSf (xn, yn, zn); }
else
// Ветвь создания цепочки четырехугольников
{
glColor3f (0.9f, 0..9f, l.Of);
glVertexSf (xi, yi, zi);
glColorSf (0.5f, 0.8f, l.0f);
glVertexSf (xj, уj, zj);
}
}
//====== Закрываем блок команд GL_QUAD_STRIP
if (!m_bQuad)
glEnd(); }
//====== Закрываем блок команд GL_QUADS
if (m_bQuad) glEnd() ;
//====== Закрываем список команд OpenGL
glEndList ();
}
Для осмысления алгоритма надо учитывать, что количество узлов сетки вдоль того или иного направления (X или Z) на единицу больше количества промежутков (ячеек). Кроме того, надо иметь в виду, что при расчете освещения OpenGL учитывает направление нормали (перпендикуляра) к поверхности. Реалистичность изображения во многом достигается благодаря аккуратному вычислению нормалей. Нормаль является характеристикой вершины (узла сетки).
  
Файловые операции
Создание тестовой поверхности, чтение данных из файла и хранение этих данных в контейнере мы будем делать так же, как и в проекте MFC. Для разнообразия используем другую формулу для описания поверхности по умолчанию, то есть того графика, который увидит пользователь элемента ActiveX при его инициализации в рамках окна контейнера. Вот эта формула:
Yi,j=exp[-(i+20*j)/256]*SIN[3*п*
(i-Nz/2)/Nz]*SIN[3*п*(j-Nx/2)/Nx]
Приведем тело функции Def aultGraphic, которая генерирует значения этой функции над дискретной сеткой узлов в плоскости X-Z и записывает их в файл с именем «expidat». В теле этой функции мы вызываем другую вспомогательную функцию SetGraphPoints, которая наполняет контейнер точек типа CPointSD. При этом, как вы помните, она генерирует недостающие две координаты (z, x) и масштабирует ординаты (у) так, чтобы соблюсти разумные пропорции изображения графика на экране:
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+1];
//====== Показываем на него указателем целого типа
UINT *p = (UINT*)buff;
// Размещаем данные целого типа
*р++ = m_xSize;
*р++ = m_zSize;
//===== Меняем тип указателя, так как дальше
//====== собираемся записывать вещественные числа
float *pf = (float*)p;
// Предварительно вычисляем коэффициенты уравнения
double fi = atan(l.)*12, kx=fi/nx, kz=fi/nz;
//=== В двойном цикле пробега по сетке узлов
//=== вычисляем и помещаем в буфер данные типа float
for (UINT i=0; i<m_zSize;
for (UINT j=0; j<m_xSize;
*pf++ = float (exp(-(i+20.*j)/256.)
*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;
}
Коды функций SetGraphPoints, ReadData и DoRead возьмите из MFC-ГфИЛО-ження OG, которое мы разработали ранее. При этом не забудьте изменить заголовки функций. Например, функция SetGraphPoints теперь является членом класса COpenGL, а не COGView, как было ранее. Кроме того, метод ReadData теперь стал экспонируемым, а это означает, что он описывается как STDMETHODIMP COpenGL: : ReadData (void) и должен возвращать значения во всех ветвях своего алгоритма. В связи с этими изменениями приведем полностью код функции ReadData.
STDMETHODIMP COpenGL::ReadData(void)
{
//=== Строка, в которую будет помещен файловый путь
TCHAR szFile[MAX_PATH] = { 0 };

//=== Строка фильтров демонстрации файлов
TCHAR *szFilter =
TEXT("Graphics Data Files (*.dat)\0")
TEXT("*.dat\0")
TEXT("All FilesX()")
TEXT("*.*\0");
//=== Выявляем текущую директорию
TCHAR szCurDir[MAX_PATH];
::GetCurrentDirectory(MAX_PATH-l,szCurDir) ;
// Структура данных, используемая файловым диалогом
OPENFILENAME ofn;
ZeroMemory(&ofn,sizeof(OPENFILENAME));
//=== Установка параметров будущего диалога
ofn.lStructSize = sizeof(OPENFILENAME) ;
//=== Окно-владелец диалога
ofn.hwndOwner = GetSafeHwnd();
ofn.IpstrFilter = szFilter;
//=== Индекс строки фильтра (начиная с единицы)
ofn.nFilterlndex= 1;
ofn.IpstrFile = szFile;
ofn.nMaxFile = sizeof(szFile);
//=== Заголовок окна диалога
ofn.IpstrTitle = _Т("Найдите файл с данными");
ofn.nMaxFileTitle = sizeof (ofn.IpstrTitle);
//=== Особый стиль диалога (только в Win2K)
ofn.Flags = OFN_EXPLORER;
//=== Создание и вызов диалога
// В случае неудачи GetOpenFileName возвращает О
if (GetOpenFileName(&ofn))
{
// Попытка открыть файл, который должен существовать
HANDLE hFile = CreateFile(ofn.IpstrFile, GENERIC READ, FILE SHARE READ, 0,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0) ;
//===== В случае неудачи CreateFile возвращает -1
if (hFile == (HANDLE)-1)
{
MessageBox(_T("He удалось открыть файл"));
return S_FALSE;
}
//=== Попытка прочесть данные о графике
if (IDoRead(hFile))
return S_FALSE;
//====== Создание нового изображения
DrawScene();
//====== Перерисовка окна OpenGL
Invalidate(FALSE);
}
return S_OK;
}
Если вы используете операционную систему Windows 2000, то файловый диалог, который создает функция GetOpenFileName, должен иметь другой стиль. Он задан флагом OFN_EXPLORER.

Установка освещения


Параметры освещения будут изменяться с помощью регуляторов, которые мы разместим на новой странице блока Property Pages. Каждую новую страницу этого блока принято реализовывать в виде отдельного интерфейса, раскрываемого специальным объектом (ко-классом) ATL. Однако уже сейчас мы можем дать тело вспомогательной функции SetLight, которая устанавливает параметры освещения, подобно тому как это делалось в уроке, где говорили о графике в рамках MFC. Параметры освещения будут храниться в массиве m_LightParam, взаимо-действовующем с диалогом, размещенным на новой странице свойств:
void COGCOpenGLView::SetLight()
{
//====== Обе поверхности изображения участвуют
//====== при вычислении цвета пикселов при
//====== учете параметров освещения
glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, 1) ;
//====== Позиция источника освещения
//====== зависит от размеров объекта
float fPosf] =
{
(m_LightParam[0]-50)*m_fRangeX/100,
(m_LightParam[l]-50)*m_fRangeY/100,
(m_LightParam[2]-50)*m_fRangeZ/100,
l.f
};
glLightfv(GL__LIGHTO, GL_POSITION, fPos);
//====== Интенсивность окружающего освещения
float f = m_LightParam[3]/100. f ;
float fAmbient[4] = { f, f, f, O.f };
glLightfv(GL_LIGHTO, GL_AMBIENT, fAmbient);
//====== Интенсивность рассеянного света
f = m_LightParam[4]/lOO.f ;
float fDiffuse[4] = { f, f, f, O.f } ;
glLightfv(GL_LIGHTO, GL_DIFFUSE, fDiffuse);
//====== Интенсивность отраженного света
f = m_LightParam[5]/l00.f;
float fSpecular[4] = { f, f, f, 0. f } ;
glLightfv(GL_LIGHTO, GL_SPECULAR, f Specular.) ;
//====== Отражающие свойства материала
//===== для разных компонентов света
f = m_LightParam[61/100.f;
float fAmbMat[4] = { f, f, f, O.f };
glMaterialfv(GL_FRONT_AND_BACK, GL__AMBIENT, fAmbMat);
f = m_LightParam[7]/l00.f;
float fDifMat[4] = {- f, f, f, l.f } ;
glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, fDifMat);
f = m_LightParam[8]/lOO.f;
float fSpecMat[4] = { f, f, f, 0.f };
glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, fSpecMat);
//======= Блесткость материала
float fShine = 128 * m_LightParam[9]/100.f;
glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, fShine);
//======= Излучение света материалом
f = m_LightParam[10]/lOO.f;
float fEmission[4] = { f, f, f, O.f };
glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, fEmission);
}
Параметры освещения
Данные о том, как должна быть освещена сцена, мы будем получать из диалоговой вкладки свойств, которую создадим позже, но сейчас можем дать коды методов обмена данными, которые являются частью интерфейса lOpenGL:
STDMETHODIMP COpenGL::GetLightParams(int* pPos)
{
//======= Проход по всем регулировкам
for (int 1=0; i<ll; i++)
//======= Заполняем транспортный массив pPos
pPos[i] = m_LightParam[i];
return S_OK;
}
STDMETHODIMP COpenGL: : SetLightParam (short lp, int nPos)
//====== Синхронизируем параметр 1р и устанавливаем
//====== его в положение nPos
m_LightParam[lp] = nPos;
//==== Перерисовываем окно с учетом изменений
FireViewChange ();
return S_OK;
}
Метод CComControl: : FireViewChange уведомляет контейнер, что объект хочет перерисовать все свое окно. Если объект в данный момент неактивен, то уведомление с помощью указателя m_spAdviseSink поступает в клиентский сток (sink), который мы рассматривали при обзоре точек соединения.
В данный момент вы можете построить DLL и посмотреть, что получилось, запустив тестовый контейнер. Однако, как это часто бывает в жизни программиста, мы не увидим ничего, кроме пустой рамки объекта. В таком состоянии можно остаться надолго, если не хватает квалификации и опыта отладки СОМ DLL-серверов. Сразу не видны даже пути поиска причины отказа. Никаких грубых промахов вроде бы не совершали. Процесс создания окна внедренного объекта происходит где-то за кадром. Опытный читатель, возможно, давно заметил неточность, которая закралась на самой начальной стадии создания заготовки ATL Control, но если опыта или знаний недостаточно, то надо все начинать заново, или рассматривать работающие примеры и скрупулезно сравнивать код. Здесь я потратил пару мучительных дней, видимо, по своей глупости, но все-таки нашел причину отказа. Она, как это тоже часто бывает, оказалась очень простой и очевидной. Мы забыли установить один флажок при создании заготовки ко-класса, который устанавливает в TRUE переменную:
CComControl::m_bWindowOnly
Наш класс GOpenGL, конечно же, унаследовал эту переменную. Она указывает СОМ, что элемент ActiveX должен создавать окно, даже если контейнер поддерживает элементы, не создающие окон. Приведем оригинальный текст: «m_bWindowOnly — Flag indicating the control should be windowed, even if the container supports win-do wless controls». Для исправления ситуации достаточно вставить в конструктор класса COpenGL такую строку:
m_bWindowOnly = TRUE;
После этого вы должны увидеть окно нашего ActiveX элемента, а в нем поверхность, вид которой показан на рис. 9.1.
Реализация методов интерфейса
Методы, обозначенные в интерфейсе IOреnсb, будут вызываться из клиентского приложения либо через IDispatch, либо с помощью страницы свойств, которую мы вскоре создадим. В любом случае, эти методы должны либо получить параметр настройки изображения и перерисовать его с учетом настройки, либо вернуть текущее состояние запрашиваемого параметра настройки:
STDMETHODIMP COpenGL::GetFillMode(DWORD* pMode)
{
//======= Режим заполнения полигонов
*pMode = m_FillMode;
return S_OK;
}
STDMETHODIMP COpenGL::SetFillMode(DWORD nMode)
m_FillMode = nMode;
//====== Построение нового списка команд OpenGL
DrawScene();
// Требование получить разрешение перерисовать окно FireViewChange();
return S_OK;
STDMETHODIMP COpenGL::GetQuad(BOOL* bQuad)
//======= Режим построения полигонов
*bQuad = m_bQuad;
return S_OK;
}
STDMETHODIMP COpenGL::SetQuad(BOOL bQuad)
{
m_bQuad = bQuad == TRUE;
//======= Построение нового списка команд OpenGL
DrawScene ();
//======= Просьба о перерисовке
FireViewChange();
return S_OK;
}
  
Страницы свойств
Перед тем как мы начнем работать с окном СОМ-объекта, вводя в него реакции на управляющие воздействия, покажем, как добавить страницу свойств (property page) в уже существующий блок страниц объекта, который активизируется с помощью контекстного меню. Страница свойств является отдельным элементом управления, называемым Property Page, интерфейсы которого должны быть реализованы в рамках отдельного ко-класса. Такая структура позволяет нескольким ко-классам одновременно пользоваться страницами свойств, размещенными в общем СОМ DLL-сервере. Новый класс для поддержки страницы свойств помещается в сервер с помощью той же процедуры, которую мы использовали при вставке класса COpenGL, но при этом следует выбрать другой тип элемента управления. Вновь воспользуемся услугами мастера Studio.Net ATL Add Class.

  1. Установите фокус на элемент ATLGL в дереве Solution Explorer и в контекстном меню выберите команду Add > Add Class, при этом важно, чтобы фокус стоял на имени проекта ATLGL
  2. В окне диалога Add Class выберите категорию ATL, шаблон ATL Property Page и нажмите кнопку Open.
  3. В окне мастера ATL Property Page выберите вкладку Names и в поле Short Name введите PropDlg.
  4. Перейдите на вкладку Attributes и просмотрите допустимые установки, ничего в них не меняя.
  5. Перейдите на вкладку Strings и в поле Title введите имя страницы Light, которое будет обозначено на вкладке (page tab). В поле Doc String введите строку Graphics Properties.
  6. Нажмите кнопку Finish.

Просмотрите результаты. Прежде всего убедитесь, что в проекте появился новый класс CPropDlg, который поддерживает функциональность страницы свойств и окна диалога. Однако, запустив сервер и вызвав из контекстного меню его свойства, вы не увидите новой страницы. Там будут только те две страницы, которые были и до момента, как вы подключили поддержку страницы свойств. Для того чтобы новая страница действительно попала в блок страниц элемента, надо ввести новый элемент в карту свойств разрабатываемого элемента COpenGL. Откройте файл OpenGL.h и найдите в нем карту свойств. Она начинается строкой:
BEGIN_PROP_MAP(COpenGL)
Введите в нее новый элемент:
PROP_ENTRY("Свет", 1, CLSID_PropDlg)
который привязывает (binds) новую страницу к существующему блоку страниц свойств. Как видите, страница создается и связывается с объектом COpenGL по правилам СОМ, то есть с помощью уникального идентификатора ко-класса CLSlD_PropDlg. Единица определяет индекс DISPID (dispatch identifier) — 32-битный идентификатор, который используется упоминавшейся выше функцией invoke для идентификации методов, свойств и аргументов. Карта свойств теперь должна выглядеть следующим образом:
BEGIN_PROP_MAP(COpenGL)
PROP_DATA_ENTRY("_cx", m_sizeExtent.ex, VT_UI4)
PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VT_UI4)
PROP_ENTRY("FillColor", DISPID_FILLCOLOR, CLSID_StockColorPage)
PROP_ENTRY("CBeT", 1, CLSID_PropDlg) END_PROP_MAP()
Здесь важно уяснить, что каждая строка типа PROP_ENTRY соответствует какой-то функциональности, скрытой в каркасе сервера. Например, стандартное свойство Fill Color реализовано с помощью одной переменной m_clrFillColor и пары функций FillColor, упоминания о которых вы видели в IDL-файле. Тела этих функций остались за кулисами. То же справедливо относительно страницы свойств.
Важным моментом является появление нового ко-класса в составе библиотеки типов, генерируемой DLL-сервером. В коде, приведенном ниже, отметьте появление строк, связанных с ко-классом PropDlg и, конечно, не обращайте внимание на идентификаторы CLSID, которые могут не совпадать даже с предыдущей версией в этой книге, так как в процессе разработки сервера мне приходится неоднократно повторять заново процесс создания ко-классов:
Примечание
Каждый раз при этом идентификаторы CLSID обновляются, и ваш реестр распухает еще больше. Хорошим правилом для запоминания в этом случае является следующее. Убирайте регистрацию всего сервера каждый раз, когда вы целиком убираете какой-либо неудачный ко-класс. Это, как мы отмечали, делается с помощью команды Start > Run > regsvr32 -u "C:\My Projects\ATLGL\ Debug\ATLGL.dll.". Перед тем как нажать кнопку ОК, внимательно проверьте правильность файлового пути к вашему серверу.
library ATLGLLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb") ;
[
uuid(6DEBB446-C43A-4AB5-BEEl-110510C7AC89)
helpstring("_IOpenGLEvents Interface")
]
dispinterface _IOpenGLEvents
{
properties:
methods:
};
[
uuid(5B3EF182-CD91-426F-9309-2E4869C353DB),
helpstringC'OpenGL Class")
]
coclass COpenGL
{
[default] interface IQpenGL;
[default, source] dispinterface _IOpenGLEvents;
};
//====== Новые элементы в библиотеке типов сервера
[
uuid(3AE16CD6-4558-460F-8A7E-5AB83D40DE9A),
helpstring("_IGraphPropEvents Interface")
]
dispinterface _IGraphPropEvents
{
properties:
methods:
};
[
uuid(lAOC756A-DA17-4630-91BO-72722950B8F7) ,
helpstring("GraphProp Class")
]
coclass PropDlg
{
interface lUnknown;
[default, source] dispinterface _IGraphPropEvents;
};
Убедитесь, что в составе проекта появились новые файлы (PropDlg. h, PropDlg. cpp и PropDlg. rgs). Откройте первый файл описаний и отметьте, что класс CPropDlg происходит от четырех родителей (классов ATL и одного интерфейса). Два из них (ccomObjectRootEx и CGomCoClass) мы уже встречали ранее, а два других (iPropertyPagelmpl и CDialoglmpl), как нетрудно догадаться, поддерживают функциональность диалоговой вкладки (страницы), размещаемой в блоке страниц (property sheet), и самого диалога, то есть механизм обмена данными. Оба родителя являются шаблонами, которые уже настроены на наш конкретный класс CPropDlg. Конструктор класса:
CPropDlg()
{
m_dwTitleID = IDSJTITLEPropDlg;
m_dwHelpFileID = IDS_HELPFILEPropDlg;
m_dwDocStringID = IDS_DOCSTRINGPropDlg;
}
устанавливает унаследованные переменные m_dwTitleio и идентификаторы строковых ресурсов в те значения, которые им присвоил мастер Studio.Net. Сами строки вы можете увидеть в ресурсах, если откроете узел дерева String Table. В классе изначально присутствует реакция на кнопку Apply, которая, как вы знаете, всегда сопровождает блок диалоговых вкладок (property sheet):
//====== Реакция на нажатие кнопки Apply
STDMETHOD(Apply)(void)
{
ATLTRACE(_T("CPropDlg::Apply\n"));
for (UINT i = 0; i < m_nObjects; i++)
{
// Do something interesting here
// ICircCtl* pCirc;
//m_ppUnk[i]->QueryInterface(IID_ICircCtl, (void**)SpCirc)
// pCirc->put_Caption(CComBSTR("smth special"));
// pCirc->Release();
}
m_bDirty = FALSE;
return S__OK;
}
В комментарий мастер поместил подсказку, которая дает намек о том, как следует пользоваться новым классом. Как вы видите, общение между двумя классами нашего сервера (copenGL и CPropDlg) должно происходить по правилам СОМ, то есть с помощью указателя на интерфейс. Этот факт производит впечатление излишней усложненности. Если оба класса расположены в рамках одной DLL, они могли бы общаться друг с другом с помощью прямого указателя, несмотря на то, что сама DLL загружается в пространство чужого процесса.
Примечание
Имя ICircCtl, которое присутствует в подсказке, не имеет отношения к нашему проекту. Оно связано с учебным примером по созданию элементов управления с помощью библиотеки ATL. Вы можете увидеть этот пример в MSDN (Visual C++ Tutorials > Creating the Circle Control).
Переменная m_bDirty используется каркасом в качестве флага доступности кнопки Apply. Если m_bDirt у == FALSE; то кнопка недоступна. Она тотчас же должна стать доступной, если пользователь страницы диалога свойств введет изменения в органы управления на лице диалога. Конечно, этим состоянием управляет разработчик, то есть мы с вами.
  
Конструируем облик страницы свойств
Важным моментом в том, что произошло, когда вы добавили страницу свойств, является появление шаблона окна диалоговой вставки IDD_PROPDLG. Сейчас вам следует сконструировать облик этой вставки, разместив на ней элементы управления, необходимые для управления освещением. Кроме того, мы поместим туда кнопку вызова файлового диалога, выпадающий список для выбора одного из трех режимов заполнения полигонов и кнопку для переключения режима генерации поверхности (GL_QUADS или GL_QUAD_STRIP). Создайте с помощью редактора диалогов окно, примерный вид которого приведен на рис. 9.2. Вы, наверное, знаете, что нижний ряд кнопок поставляется блоком страниц (property sheet) и вам их вставлять не надо, необходимо сконструировать только облик самой страницы.
На рисунке показано окно диалога в активном состоянии, но вам еще предстоит поработать, чтобы довести его до этого вида. Здесь очень важно не торопиться и быть внимательным. Опыт преподавания в MS Authorized Educational Center (www.Avalon.ru) подтверждает, что большая часть ошибок вносится на стадии работы с ресурсами. Визуальные редакторы расслабляют внимание, и ошибки появляются там, где вы меньше всего их ждете.
В основных чертах окно имеет тот же облик, что и окно диалога по управлению освещением сцены, разработанное ранее (в MFC проекте). Но здесь есть два новых элемента, функциональность которых ранее была спрятана в командах меню. Так как в рамках этого проекта мы не имеем меню, то нам пришлось использовать элементы управления, сосредоточенные в нижней части окна диалоговой вставки. Во-первых, не забудьте, что справа от каждого ползунка вы должны расположить элемент типа static Text, в окне которого будет отражено текущее положение движка в числовой форме.
Кнопка Выбор файла, как и ранее, позволяет пользователю открыть файловый диалог и выбрать файл с данными для нового изображения. Выпадающий список Заполнение позволяет выбрать режим изображения полигонов (GL_FILL, GL_POINT или GL_LINE), а кнопка Quads/Strip изменяет режим использования примитивов при создании поверхности. Идентификаторы элементов управления мы сведем в табл. 9.1.
Таблица 9.1. Идентификаторы элементов управления

Элемент

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

/ Диалог

IDD_PROPDLG

Ползунок Общая в группе Освещенность

IDC_AMBIENT

Ползунок Рассеянная в группе Освещенность

IDC_DIFFUSE

Ползунок Отраженная в группе Освещенность

IDC_SPECULAR

Text справа от Общая в группе Освещенность

IDC_AMB_TEXT

Text справа от Рассеянная в группе Освещенность

IDC_DIFFUSE_TEXT

Text справа от Отраженная в группе Освещенность

IDC_SPECULAR_TEXT

Ползунок Общая в группе Материал

IDC_AMBMAT

Ползунок Рассеянная в группе Материал

IDC_DIFFMAT

Ползунок Отраженная в группе Материал

IDC.SPECMAT

Text справа от Общая в группе Материал

IDC_AMBMAT_TEXT

Text справа от Рассеянная в группе Материал

IDC_DIFFMAT_TEXT

Text справа от Отраженная в группе Материал

IDC_SPECMAT_TEXT

Ползунок Блестскость

IDC_SHINE

Ползунок Эмиссия

IDC.EMISSION

Text справа от Блестскость

IDC_SHINE_TEXT

Text справа от Эмиссия

IDC_EMISSION_TEXT

Ползунок X

IDC_XPOS

Ползунок Y

IDC.YPOS

Ползунок Z

IDC_ZPOS

Text справа от X

IDC_XPOS_TEXT

Text справа от¥

IDC_YPOS_TEXT

Text справа от Z

IDC_ZPOS_TEXT

Выпадающий список Заполнение

IDC_FILLMODE

Кнопка Quads

IDC.QUADS

Кнопка Выбор файла

IDC_FILENAME

Вместо кнопки Quads просится пара переключателей (radio button) Quads/Strip. Сначала я так и сделал, но потом, к сожалению, пришлось отказаться из-за сложностей введения отклика реакции или уведомления, на выбор, произведенный в группе переключателей. Они обусловлены несовершенством бета-версии Studio.Net. Если вы впервые устанавливаете группу переключателей (radio buttons), то вам следует знать, что группа Quads/Strip будет работать правильно, если числовые значения идентификаторов составляющих ее элементов следуют подряд и (только) для первого переключателя установлено свойство Group. Для второго этот флаг должен быть снят. Если вы вставляете еще одну группу, то картина должна повториться. Первый переключатель должен иметь свойство Group в положении True, а остальные (если их много) — нет.
Для того чтобы просмотреть числовые значения идентификаторов, следует поставить фокус на элемент IDD_PROPDLG в дереве ресурсов (в окне Resource View) и вызвать контекстное меню. Затем надо выбрать команду Resource Symbols. Появится диалог со списком всех идентификаторов, которые хранятся в файле resource.h. Не следует редактировать этот файл вручную.
Примечание
Изменять числовые значения идентификаторов следует с большими предосторожностями, так как ошибки на этом этапе могут внести трудно распознаваемые отказы и нестабильную работу приложения. Надо сказать, что отслеживание корректности числовых значений идентификаторов всегда было слабым местом как Visual Studio, так и среды разработки Borland. Беру на себя смелость предположить, что уйма времени была затрачена разработчиками всех стран на поиск ошибок такого рода, так как сам потратил много усилий и времени пока не понял, что легче уничтожить ресурс и создать заново, чем пытаться найти новый диапазон числовых значений, который не затронет другие идентификаторы.
Если, несмотря на предостережения, вам захочется изменить числовое значение какого-либо идентификатора, то можете это сделать в окне Properties.

  1. Поставьте фокус на элемент управления, идентификатор которого вас не устраивает, и перейдите в окно Properties.
  2. В конец строки с идентификатором добавьте текст вида «=127», где 127 — новое значение идентификатора. Например, IDC_QUAD=127.

Редактор ресурсов может с возмущением отвергнуть ваш выбор. Тогда ищите другой диапазон с помощью уже рассмотренного диалога Resource Symbols. Эта тактика потенциально опасна. Повторюсь и скажу, что проще удалить и создать заново весь ресурс. Однако если вы самостоятельно выработаете или узнаете о более надежной технологии, то прошу сообщить мне. В этот момент следует запустить сервер и проверить наличие элементов на новой странице свойств. Если что-то не так, надо внимательно проверить, а возможно, и повторить все шаги создания вкладки.
  
Взаимодействие классов
Класс CPropDlg должен обеспечить реакцию на изменение регулировок, а класс COpenGL должен учесть новые установки и перерисовать изображение. Общение классов, как мы уже отметили, происходит по законам СОМ, то есть с помощью указателя на интерфейс. Здесь нам на помощь приходит шаблон классов CComQiPtr. Литеры «QI» в имени шаблона означают Querylnterface, что обещает нам автоматизацию в реализации запроса указателя на этот интерфейс. В классе переопределены операции выбора (->), взятия адреса (&), разадресации (*) и некоторые другие, которые упрощают использование указателей на различные интерфейсы. При создании объекта класса CComQiPtr, например:
CComQIPtr<IOpenGL, &IID_IOpenGL> р(m_ppUnk[i]) ;
он настраивается на нужный нам интерфейс, и далее мы работаем с удобствами, не думая о функциях Querylnterface, AddRef и Release. При выходе из области действия объекта р класса CGomQiPtr<lOpenGL, &ilD_iOpenGL> освобождение интерфейса произойдет автоматически.
Для обмена с окном диалоговой вставки введите в protected-секцию класса CPropDlg массив текущих позиций регуляторов и переменную для хранения текущего режима изображения полигонов:
protected:
int m_Pos[11];BOOL m_bQuad;
В конструктор класса добавьте код инициализации массива:
ZeroMemory (m_Pos, sizeof(m_Pos));
Другую переменную следует инициализировать при открытии диалога (вставки). Способом, который вы уже неоднократно применяли, введите в класс реакции на Windows-сообщения WM_INITDIALOG и WM_HSCROLL. Затем перейдите к созданной мастером заготовке метода Onl nit Dialog, которую найдете в файле PropDlg.cpp:
LRESULT CPropDlg::OnInitDialog(UINT uMsg, WPARAM wParam,
LPARAM IParam, BOOL& bHandled)
{
_super::OnInitDialog(uMsg, wParam, IParam, bHandled);
return 1;
}
Здесь вы увидите новое ключевое слово языка _ super, которое является спецификой Microsoft-реализации. Оно представляет собой не что иное, как явный вызов родительской версии функции метода базового или super-класса. Так как классы в ATL имеют много родителей, то _ super обеспечивает выбор наиболее подходящего из них. Теперь введите изменения, которые позволят при открытии вкладки привести наши регуляторы в соответствие со значениями переменных в классе COpenGL. Вы помните, что значения регулировок используются именно там. Там же они и хранятся:
LRESULT CPropDlg: :OnInitDialog (UINT uMsg, WPARAM wParam,
LPARAM IParam, BOOL& bHandled)
_super::OnInitDialog(uMsg, wParam, IParam, -bHandled);
//====== Кроим умный указатель по шаблону IQpenGL
CComQIPtr<IOpenGL> p(m_ppUnk[0]);
//=== Пытаемся связаться с классом COpenGL и выяснить
//=== значение переменной m_FillMode
//=== В случае неудачи даем сообщение об ошибке
DWORD mode;
if FAILED (p->GetFillMode(&mode))
{
ShowError();
return 0;
}
//====== Работа с combobox по правилам API
//====== Получаем Windows-описатель окна
HWND hwnd = GetDlgItem(IDC_FILLMODE);
//====== Наполняем список строками текста
SendMessage(hwnd, CB_ADDSTRING, 0, (LPARAM)(LPCTSTR)"Points"
SendMessage(hwnd, CB_ADDSTRING, 0, (LPARAM)(LPCTSTR)"Lines")
SendMessage(hwnd, CB_ADDSTRING, 0, (LPARAM)(LPCTSTR)"Fill");
// Выбираем текущую позицию списка в соответствии
// со значением, полученным из COpenGL WPARAM
w = mode == GL_POINT ? 0
: mode == GL_LINE ?1:2;
SendMessage(hwnd, CB_SETCURSEL, w, 0);
// Повторяем сеанс связи, выясняя позиции ползунков
if FAILED (p->GetLightParams(m_Pos))
{
ShowError();
return 0;
}
// Мы не надеемся на упорядоченность идентификаторов
// элементов и поэтому заводим массив отображений
UINT IDs[] =
{
IDC_XPOS,
IDC_YPOS,
IDC_ZPOS,
IDC_AMBIENT,
IDC_DIFFUSE,
IDC_SPECULAR,
IDC_AMBMAT,
IDC_DIFFMAT,
IDC_SPECMAT,
IDC_SHINE,
IDC_EMISSION
};
//=== Пробег по всем регуляторам и их установка
for (int i=0;
Ksizeof (IDs)/sizeof (IDs [0] ) ; i++)
{
//====== Получаем описатель окна
hwnd = GetDlgItem(IDs[i]);
UINT nID;
//====== Узнаем идентификатор элемента
int num = GetSliderNum(hwnd, nID);
//====== Выставляем позицию
~ SendMessage(hwnd,TBM_SETPOS,TRUE,(LPARAM)m_Pos[i]
//=== Приводим в соответствие текстовый ярлык
char s [ 8 ] ;
sprintf (s,"%d",m_Pos[i]);
SetDlgltemText(nID, s);
}
// Выясняем состояние режима изображения полигонов
if FAILED (p->GetQuad(&m_bQuad))
{
ShowError ();
return 0;
}
//====== Устанавливаем текст
SetDlgltemText (IDC_QUADS,m_bQuad ? '"Quads" : "Strips");
return 1 ;
}
В процессе обработки сообщения нам понадобились вспомогательные функции GetSliderNum и ShowError. Первая функция уже участвовала в проекте на основе MFC, поэтому мы лишь напомним, что она позволяет по известному Windows-описателю окна элемента управления получить его порядковый номер в массиве позиций регуляторов. Кроме этого, функция позволяет получить идентификатор элемента управления nio, который нужен для управления им, например: при вызове SetDlgltemText (nID, s);.
int CPropDlg: : GetSliderNum (HWND hwnd, UINT& nID)
{
// Получаем ID по известному описателю окна
switch (: :GetDlgCtrlI)(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 = 1DC_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;
}
Функция showError демонстрирует, как в условиях СОМ можно обработать исключительную ситуацию. Если мы хотим выявить причину ошибки, спрятанную в HRESULT, то следует воспользоваться методом GetDescription интерфейса lErrorinfо. Сначала мы получаем указатель на него с помощью объекта класса ccomPtr. Этот класс, так же как и CGomQiPtr, автоматизирует работу с методами главного интерфейса lUnknown, за исключением метода Queryinterface:
void CPropDlg::ShowError()
{
USES_CONVERSION;
//====== Создаем инерфейсный указатель
CComPtr<IErrorInfo> pError;
//====== Класс для работы с Unicode-строками
CComBSTR sError;
//====== Выясняем причину отказа
GetErrorlnfo (0, &pError);
pError->GetDescription(SsError);
// Преобразуем тип строкового объекта для вывода в окно MessageBox(OLE2T(sError),_T("Error"),MB_ICONEXCLAMATION);
}
Если вы построите сервер в таком виде, то вас встретит неприятное сообщение о том, что ни один из явных или неявных родителей CPropDlg не имеет в своем составе функции OninitDialog. Обращаясь за справкой к документации (по классу CDialogimpl), мы убеждаемся, что это действительно так. Значит, инструмент Studio.Net, который создал заготовку функции обработки, не прав. Но как же будет вызвана наша функция OninitDialog, если она не является виртуальной функцией одного из базовых классов? Ответ на этот вопрос, как и на большинство других, можно получить в режиме отладки.
Закомментируйте строку вызова родительской версии, которая производится с помощью многообещающего ключевого слова _super (это и есть лекарство), поставьте точку останова на строке, следующей за ней, и нажмите F5. Если вы не допустили еще одной, весьма вероятной, ошибки, то тестовый контейнер сообщит, что он не помощник в процессе отладки, так как не содержит отладочной информации. Согласитесь с очевидным фактом, но не делайте поспешного вывода о том, что невозможно отлаживать все СОМ-серверы. В тот момент, когда вы инициируете новую страницу свойств, отладчик возьмет управление в свои руки и остановится на нужной строке программы. Теперь вызовите одно из самых полезных окон отладчика по имени Call stack, в нем вы увидите историю вызова функции OninitDialog, то есть цепочку вызовов функций. Для этого:

  1. Дайте команду Debug > Windows > Call Stack (или Alt+7).
  2. Внедрите это окно, если необходимо, в блок окон отладчика (внизу экрана).
  3. Убедитесь, что вызов произошел из функции DialogРгос одного из базовых классов, точнее шаблонов классов, CDialoglmplBaseT.

Этот опыт иллюстрирует тот факт, что все необычно в мире ATL. Этот мир устроен совсем не так, как MFC. Шаблоны классов дают удивительную гибкость всей конструкции, способность приспосабливаться и подстраиваться. Теперь рассмотрим вторую, весьма вероятную, ошибку. Секцию protected в классе CPropDlg следует правильно разместить (странно, не правда ли?). Лучше это сделать так, чтобы сразу за ней шло объявление какой-либо из существующих секций public. Если поместить ее, например, перед макросом
DECLARE_REGISTRY_RESOURCEID(IDR__PROPDLG)
то макрос окажется безоружным против такой атаки, хотя по идее он должен сопротивляться и даже не замечать наскоков подобного рода. Возможно, этот феномен исчезнет в окончательной версии Studio.Net.

 

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