Трехмерная
графика в проекте ATL
Требования OpenGL
Введение обработчиков
сообщений Windows
Управление цветом фона
Подготовка сцены OpenGL
Файловые операции
Установка освещения
Реализация методов интерфейса
Страницы свойств
Взаимодействие классов
Управление объектом с
помощью мыши
Создание контейнера на
базе MFC
Класс-оболочка
В этом уроке
мы продолжим разработку DLL-модуля, который после регистрации в системе в качестве
СОМ-объекта позволит любому другому клиентскому приложению, обладающему свойствами
контейнера объектов СОМ использовать его для отображения в контексте OpenGL
трехмерного графика функции, заданной произвольным массивом чисел. Данные для
графика СОМ-объект берет из файла, на который указывает пользователь клиентского
приложения. Кроме этого, объект предоставляет клиенту возможность перемещения
графика вдоль трех пространственных осей, вращения вокруг вертикальной и горизонтальной
осей и просмотра как в обычном, так и скелетном режиме. Регулируя параметры
освещения поверхности, пользователь может добиться наибольшей реалистичности
изображения, то есть усилить визуальный эффект трехмерного пространства на плоском
экране.
Графики могут
представлять результаты расчета какого-либо физического поля, например поверхности
равной температуры, давления, скорости, индукции, напряжения и т. д. в части
трехмерного пространства, называемой расчетной областью. Пользователь объекта
должен заранее подготовить данные и записать их в определенном формате в файл.
Объект по команде пользователя считывает данные, нормирует, масштабирует и изображает
в своем окне, внедренном в окно приложения-клиента. Пользователь, манипулируя
мышью, управляет местоположением и вращением графика, а открыв стандартный диалог
Properties, изменяет другие его атрибуты.
ATL (Active
Template Library) — это библиотека шаблонов функций и классов, которая разработана
с целью упрощения и ускорения разработки СОМ-объектов. Несмотря на заявления
о том, что ATL не является альтернативой MFC, а лишь дополняет ее, побудительной
причиной разработки этой библиотеки был тот факт, что объекты СОМ, разработанные
с помощью MFC, и внедренные в HTML-документ, работали слишком медленно. Наследование
от cobject и все те удобства, которые оно приносит, обходятся слишком дорого
в смысле быстродействия, и в условиях web-страницы объекты MFC-происхождения
проигрывают объектам, разработанным с помощью COM API. В библиотеке ATL не используется
наследование от cobject и некоторые другие принципы построения классов, характерные
для MFC. За счет этого удалось повысить эффективность работы СОМ-объектов и
ускорить их функционирование даже в условиях web-страниц. Пользуясь справкой
(Help), вы, наверное, видели, что многие оконные методы реализованы не только
в классе cwnd, но и в классе cwindow. Последний является классом из иерархии
библиотеки ATL, и именно он является главной фигурой при разработке окон СОМ-объектов.
Требования
OpenGL
Вместо тестового
изображения с надписью ATL 4.0, которым мы научились кое-как управлять, поместим
в окно СОМ-объекта OpenGL-изображение поверхности в трехмерном пространстве.
Точнее, мы хотим дать клиенту нашего СОМ-объекта возможность пользоваться всей
той функциональностью, которая была разработана в уроке 7. Вы помните, что изображение
OpenGL может быть создано в окне, которое прошло специальную процедуру подготовки.
Необходимо создать и сделать текущим контекст передачи OpenGL (HGRC). Вы также
помните, что подготовку контекста надо рассматривать как некий обязательный
ритуал, в котором порядок действий определен. Повторим его:
установка стиля окна;
обработка сообщения
WM_ERASEBACKGROUND и отказ от стирания фона;
установка pixel-формата;
создание контекста устройства
(нос) и контекста передачи (HGLRC);
специфическая обработка
сообщения WM_SIZE;
обработка сообщения WM_PAINT;
освобождение контекстов
при закрытии окна.
Чтобы использовать
функции библиотеки OpenGL, надо подключить их к проекту. На этапе компоновки
они будут интегрированы в коды СОМ-сервера.
В окне Solution Explorer
поставьте фокус на строку с именем проекта ATLGL и нажмите кнопку Properties,
которая расположена на панели инструментов этого окна.
В левом окне диалога
ATLGL Property Pages найдите и выберите ветвь дерева Linker.
В раскрывшемся поддереве
выберите ветвь Input и перейдите в строку Additional Inputs в таблице правого
окна.
Поставьте фокус во вторую
колонку этой строки и в конец существующего текста ячейки добавьте, не стирая
содержимое ячейки, имена подключаемых библиотек OPENGL32.LIB GLU32.LIB, не
забыв о разделяющих пробелах. Нажмите ОК.
В конец файла библиотечных
заголовков stdafx.h добавьте строки:
#include
<math.h>
#include
<gl/gl.h>
#
include
<gl/glu.h>
При работе
с трехмерными координатами мы пользовались вспомогательным классом CPoint3D,
который здесь нам тоже понадобится. Нужны будут и все переменные, которые были
использованы ранее для управления сценой OpenGL. Там, если вы помните, был контейнер
STL типа vector для хранения точек изображения. Использование контейнеров требует
подключения соответствующих файлов заголовков, поэтому вставьте в конец файла
stdafx.h следующие строки:
#include
<vector>
using namespace std;
Так как мы
собираемся демонстрировать в окне OpenGL графики функций, диапазон изменения
которых нам заранее не известен, то следует использовать предварительное масштабирование
координат точек графика. Нам надо знать габариты изображаемого объекта и для
упрощения этой задачи введем вспомогательную глобальную функцию корректировки
экстремумов:
inline
void MinMax
(float
d,
floats Min,
floats Max)
{
if
(d > Max) Max = d;
else
if (d < Min)
Min
= d;
}
Описатель inline
сообщает компилятору, что функцию можно не реализовывать в виде отдельной процедуры,
а ее тело желательно вставлять в точки вызова, с тем чтобы убрать код обращения
к стеку. Окончательное решение при этом остается за компилятором.
Введение методов в интерфейс IOpenGL
На этом этапе
важно решить, какие данные (свойства) и методы класса будут экспонироваться
СОМ-объектом, а какие останутся в качестве служебных, для внутреннего пользования.
Те методы и свойства, которые будут экспонированы, должны быть соответствующим
образом отражены в IDL-файле. Те, которые нужны только нам, останутся внутри
сервера. Для примера введем в число экспонируемых методов функцию GetLightParams,
которая определяет действующие параметры освещения.
Поставьте фокус на строку
с именем интерфейса lOpenGL в окне CLassView и вызовите контекстное меню.
Выберите команду Add
> Add Method В окне мастера Add Method Wizard введите в поле Method
Name имя метода GetLightParams. В поле Parameter Name введите имя параметра
pPos, в поле Parameter Type: — тип параметра int*, задайте атрибут параметра,
установив флажок out, и нажмите кнопку Add.
Нажмите кнопку Finish.
Проанализируйте
изменения, которые появились в IDL-файле, в файле OpenGLh и в файле OpenGLcpp.
В первом из перечисленных файлов появилось новое, уточненное описание метода
интерфейса
1:
interface
lOpenGL : IDispatch
{
[propput,
bindable, requestedit, id(DISPID_FILLCOLOR)]
HRESULT
FillColor([in]OLE_COLOR clr);
[propget,
bindable, requestedit, id(DISPID_FILLCOLOR)]
HRESULT
FillColor([out,
retval]OLE_COLOR* pclr);
[id(l),
helpstring("method GetLightParams")]
HRESULT
GetLightParams([out] int* pPos);
};
в файле заголовков
появилась строка декларации метода ко-класса, который реализует функциональность
интерфейса:
STDMETHODIMP
GetLightParams(int* pPos);
и, наконец, в файле реализации ко-класса появилась стартовая заготовка тела
метода:
STDMETHODIMP
COpenGL::GetLightParams(int *pPos)
{
//
TODO: Add your implementation code here
return
S_OK;
}
Повторите описанные
действия и введите в интерфейс еще один метод SetLightParam, который изменяет
один из параметров освещения сцены OpenGL. При задании параметров этого метода
добейтесь такого описания в окне Parameter List:
[in]
short lp [in]
int nPos;
Введите в состав
интерфейса еще один метод ReadData, на сей раз без параметров. Он будет реагировать
на кнопку и производить чтение файла с данными о новом графике. Для управления
обликом поверхности графика нам понадобятся две пары методов типа get-set. Введите
в интерфейс следующие методы:
GetFillMode
с параметром
[out] DWORD* pMode;
SetFillMode
С параметром
[in] DWORD nMode;
GetQuad
с параметром
[out] BOOL* bQuad;
SetQuad
с параметром
[in] BOOL bQuad.
Найдите новые
методы в IDL-файле и убедитесь, что мастер автоматически пронумеровал методы
(1,2,...), присвоив им индексы типа DISPID:
[id(l),
helpstring("method GetLightParams")]
HRESULT
GetLightParams([out] int* pPos);
[id(2),
helpstring("method SetLightParam")]
HRESULT
SetLightParam([in] short Ip, [in] int nPos);
[id(3),
helpstring("method ReadData")]
HRESULT
ReadData(void);
[id(4),
helpstring("method GetFillMode")]
HRESULT
GetFillMode([out] DWORD* pMode);
[id(5),
helpstring("method SetFillMode")]
HRESULT
SetFillMode([in] DWORD nMode);
[id(6),
helpstring("method GetQuad")]
HRESULT
GetQuad([out] BOOL* bQuad);
[id(7),
helpstring("method SetQuad")]
HRESULT
SetQuad([in] BOOL bQuad);
С помощью этих
индексов методы будут вызываться клиентами, получившими указатель на интерфейс
диспетчеризации IDispatch. Мы уже обсуждали способ, который используется при
вызове методов по индексам DISPID. Непосредственный вызов производит метод IDispatch:
: invoke. Тот факт, что наш объект поддерживает IDispatch, мы определили при
создании ATL-заготовки. Если вы не забыли, то мы тогда установили переключатель
типа интерфейса в положение Dual. Это означает, что объект будет раскрывать
свои методы как с помощью vtable, так и с помощью IDispatch. Некоторые детали
этого процесса обсуждались в предыдущем уроке.
Ручная
коррекция класса
Класс COpenGL
будет обслуживать окно внедренного СОМ-объекта. Он должен иметь достаточное
количество данных и методов для управления изображаемой поверхностью, поэтому
далее вручную введем сразу много изменений в файл с описанием класса COpenGL.
При изменении файла заголовков класса мы нарушим стиль, заданный стартовой заготовкой,
и вернемся к более привычному, принятому в MFC-приложениях. Перенесем существующее
тело конструктора, а также функции OnDraw в файл реализации класса OpenGLcpp.
В файле OpenGLh останутся только декларации этих функций. Ниже приведено полное
описание класса COpenGL с учетом нововведений, упрощений и исправлений. Вставьте
его вместо того текста, который есть в файле OpenGLh. После этого вставим в
файл новые сущности с помощью инструментов Studio.Net:
//
OpenGL.h : Declaration of the COpenGL
#pragma
once
#include
"resource.h" // main symbols
#include
<atlctl.h>
#include
"_IOpenGLEvents_CP.h"
//==========
Вспомогательный класс
class
CPointSD
public:
fldat
x;
float
y;
float
z; // Координаты точки в 3D
//======
Набор конструкторов и операция присвоения
CPoint3D
() { х = у = z = 0; }
CPoint3D
(float cl,
float c2,
float c3)
x
= с1;
z
= c2;
у
= сЗ;
CPoint3D&
operator=(const
CPoint3D& pt)
x
= pt.x;
z
= pt. z ;
У
= pt.y;
return
*this;
}
CPointSD
(
const CPoint3D& pt)
*this = pt;
//====
Основной класс, экспонирующий интерфейс IQpenGL
class
ATL_NO_VTABLE COpenGL :
p.ublic
CQomObjectRootEx<CComSingleThreadModel>,
public
CStockPropImpKCOpenGL, IOpenGL>,
public
IPersistStreamInitImpl<COpenGL>,
public
I01eControlImpl<COpenGL>,
public
I01eObjectImpl<COpenGL>,
public
I01eInPlaceActiveObjectImpl<COpenGL>,
public
IViewObjectExImpl<COpenGL>,
public
I01eInPlaceObjectWindowlessImpl<COpenGL>,
public
ISupportErrorlnfo,
public
IConnectionPointContainerImpl<COpenGL>,
public
CProxy_IOpenGLEvents<COpenGL>,
public
IPersistStorageImpl<COpenGL>,
public
ISpecifyPropertyPagesImpl<COpenGL>,
public
IQuickActivateImpl<COpenGL>,
public
IDataObjectImpl<COpenGL>,
public
IProvideClassInfo2Impl<&CLSID_OpenGL,
&_uuidof(_IOpenGLEvents),
&LIBID_ATLGLLib>,
public
CComCoClass<COpenGL, &CLSID_OpenGL>,
public
CComControl<COpenGL>
{
public:
//=====
Переменные, необходимые
Угол
поворота вокруг оси X
Угол
поворота вокруг оси Y
Режим
заполнения полигонов
Флаг
использования GL QUAD
Текущий
размер окна вдоль X
Текущий
размер окна вдоль Y
//======
Массив вершин поверхности
vector
<CPoint3D> m_cPoints;
//======
Функции, присутствовавшие в стартовой заготовке
COpenGL();
HRESULT
OnDraw(ATL DRAWINFO& di);
void
OnFillColorChangedO ;
DECLARE_OLEMISC_STATUS(OLEMISC_RECOMPOSEONRESIZE
OLEMISC_CANTLINKINSIDE
|
OLEMISC_INSIDEOUT
|
OLEMISC_ACTIVATEWHENVISIBLE
|
OLEMISC_SETCLIENTSITEFIRST
|
DECLARE_REGISTRY_RESOURCEID(IDR_OPENGL)
BEGIN_COM_MAP(COpenGL)
COM_INTERFACE_ENTRY(IQpenGL)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IViewObj
ectEx)
COM_INTERFACE_ENTRY(IViewObj
ect2)
COM_INTERFACE_ENTRY(IViewObj
ect)
COM_INTERFACE_ENTRY(I01eInPlaceObjectWindowless)
COM_INTERFACE_ENTRY(I01eInPlaceObject)
COM_INTERFACE_ENTRY2(IQleWindow,
IQlelnPlaceObjectWindowless)
COM_INTERFACE_ENTRY(lOlelnPlaceActiveObject)
COM_INTERFACE_ENTRY(lOleControl)
COM_INTERFACE_ENTRY(lOleObj
ect)
COM_INTERFACE_ENTRY(IPersistStreamInit)
COM_INTERFACE_ENTRY2(IPersist,
IPersistStreamlnit)
COM_INTERFACE_ENTRY(ISupportErrorlnfo)
COM_INTERFACE_ENTRY(IConnectionPointContainer)
COM_INTERFACE_ENTRY(ISpecifyPropertyPages)
COM_INTERFACE_ENTRY(IQuickActivate)
COM_INTERFACE_ENTRY(IPersistStorage)
COM_INTERFACE_ENTRY(IDataObject)
COM_INTERFACE_ENTRY(IProvideClassInfo)
COM_INTERFACE_ENTRY(IProvideClassInfo2)
END_COM_MAP()
BEGIN_PROP_MAP(COpenGL)
PROP_DATA_ENTRY("_cx",
m_sizeExtent. ex, VTJJI4)
PROP_DATA_ENTRY("_cy",
m_sizeExtent.cy, VTJJI4) PROP_ENTRY("FillColor",DISPID_FILLCOLOR, CLSID_StockColorPage)
END_PROP_MAP()
BEGIN_CONNECTION_POINT_MAP(COpenGL)
CONNECTION_POINT_ENTRY(DIID_IQpenGLEvents)
END_CONNECTION_POINT_MAP()
BEGIN_MSG_MAP(COpenGL)
CHAIN_MSG_MAP(CComControKCOpenGL>)
DEFAULT_REFLECTION_HANDLER()
END_MSG_MAP()
//======
Поддержка интерфейса ISupportsErrorlnfо STDMETHOD(InterfaceSupportsErrorlnfo)(REFIID
riid)
{
static
const IID* arr[] =
{
&IID_IOpenGL,
};
for
(int i=0;
ixsizeof(arr)/sizeof(arr[0]); i++)
{
if
(InlineIsEqualGUID(*arr[i], riid))
return
S_OK;
}
return
S_FALSE;
}
//======
Поддержка интерфейса IViewObjectEx
DECLARE_VIEW_STATUS(VIEWSTATUS_SOLIDBKGND
| VIEWSTATUS_OPAQUE)
//======
Поддержка интерфейса IQpenGL
public:
DECLARE_PROTECT_FINAL_CONSTRUCT()
HRESULT
FinalConstruct()
{
return
S_OK;
}
void
FinalRelease()
{
}
//======
Экспонируемые методы
STDMETHODIMP
GetLightParams(int* pPos);
STDMETHODIMP
SetLightParam(short Ip, int nPos);
STDMETHODIMP
ReadData(void);
//======
Новые методы класса
//======
Установка параметров освещения
void
SetLight ();
//======
Создание демонстрационного графика
void
DefaultGraphic();
//======
Чтение файла с данными о графике
bool
DoRead(HANDLE hFile);
//
Заполнение координат точек графика по данным из буфера
void
SetGraphPoints(BYTE* buff, DWORD nSize);
//======
Управление цветом фона окна
void
SetBkColor ();
//==
Создание изображения в виде списка команд OpenGL
void
DrawScene();
};
OBJECT
ENTRY AUTO (_
uuidof (OpenGL) , COpenGL)
Обзор
класса COpenGL
Начальные строки
кода класса должны показаться вам знакомыми, так как вы уже знаете, что мастер
ATL ControlWizard предоставляет ко-классу множество родителей для обеспечения
той функциональности, которая была заказана при создании стартовой заготовки.
Макрос DECLARE_OLEMISC_STATUS задает набор битовых признаков, собранных в тип
перечисления OLEMISC (miscellaneous — разнообразные, не принадлежащие одной
стороне описания). Они описывают различные характеристики СОМ-объекта или класса.
Контейнер может выяснить эти параметры с помощью метода lOleObject: :GetMiscStatus.
Некоторые настройки попадают в специальный раздел реестра для сервера CLSiD\MiscStatus.
Мы видим, что в заготовке присутствуют следующие биты:
OLEMISC_RECOMPOSEONRESIZE
— сообщает контейнеру, что при изменении размеров окна объекта последний хочет
не просто изменить пропорции, но и выполнить более сложную рекомпозицию. Отзывчивый
контейнер должен запустить сервер и вызвать метод lOleObject: :SetExtent,
передав новый размер окна;
OLEMISC_CANTLINKINSIDE
— говорит о том, что после передачи объекта контейнером он может быть выбран,
но при этом не может открыться в режиме для редактирования, то есть при помещении
объекта в буфер обмена контейнер может предоставить свою связь (link), но
не связь с объектом;
OLEMISC__INSIDEOUT —
объект способен к активизации на месте (in place), но при этом не требуется
изменять меню и инструментальную панель в рамках контейнера;
OLEMISC__ACTIVATEWHENVISIBLE
— этот признак устанавливается одновременно с предыдущим и говорит о том,
что объект хочет быть активным всякий раз, когда он становится видимым. Некоторые
контейнеры могут и предпочитают игнорировать это указание;
OLEMISC_SETCLIENTSITEFIRST
— этот признак характерен для всех средств управления (controls) и он говорит
о том, что в качестве функции инициализации следует вызвать функцию lOleObject:
: SetClientSite, которая позволяет определить свойства окружения (ambient
properties), до того как будут загружена информация из хранилища (persistent
storage). Далеко не все контейнеры способны учесть это указание.
Карты
интерфейсов и свойств
Далее по коду
вы видите карту макросов COM map, которая скрывает механизм предоставления клиенту
интерфейсов с помощью метода Querylnterf асе (vtable-интерфейсы). Как вы можете
видеть, каркас сервера предоставляет и поддерживает достаточно много интерфейсов,
не требуя от нас каких-либо усилий. За СОМ-картой следует карта свойств (см.
BEGIN_PROP_MAP), которая хранит такие описания свойств, как индексы диспетчеризации
типа DISPID, индексы страниц свойств (property pages) типа CLSID, а также индекс
интерфейса IDispatch типа iID. Если обратиться к документации, то там сказано,
что имя PROP_DATA_ ENTRY является именем функции, а не макросом, как естественно
было бы предположить. Вызов этой функции делает данные, которые заданы параметрами,
устойчивыми (persistent). Это означает, что если приложение-клиент сохраняет
свой документ с внедренным в его окно элементом ActiveX, то размеры m_sizeExtent,
заданные параметром функции, тоже будут сохранены. Немного ниже будет описано,
как вставить в карту элемент, описывающий новую страницу свойств.
Карта
точек соединения
Следующая карта
BEGIN_CONNECTION_POINT_MAP описывает интерфейсы точек соединения (или захвата),
которые характерны для соединяемых (connectable) СОМ-объектов. Так называются
объекты, которые предоставляют клиенту исходящие (outgoing) интерфейсы.
Примечание
Интерфейсы, раскрываемые
с помощью рассмотренного механизма Querylnterface, называются входящими (incoming),
так как они входят в объект (запрашиваются) со стороны клиента. Как отмечает
Kraig Brockschmidt (в уже упоминавшейся книге Inside OLE), входящие интерфейсы
являются глазами и ушами СОМ-объекта, которые воспринимают сигналы из окружающего
мира. Но некоторые объекты могут не только слушать, но и сказать нечто полезное.
Это требует от клиента способности к диалогу. Двусторонний диалог подразумевает
наличие исходящих (outgoing) интерфейсов и особого механизма общения, основанного
на обработке событий (events), уведомлений (notifications) или запросов (requests).
События и запросы
сходны с Windows-сообщениями, которые также информируют окно о каком-то событии
(WM_SIZE, WM_COMMAND) или запрашивают какие-то данные (WM_CTLCOLOR, WM_QUERYENDSESSION).
Точки связи (connection points) предоставляются объектом для каждого исходящего
из него интерфейса. Клиент, умеющий слушать, реализует эти интерфейсы с помощью
объекта, называемого sink (сток, слив). Его можно представить себе в виде воронки,
которую клиент подставляет для того, чтобы объект мог сливать в нее свои сообщения.
С точки зрения стока исходящие (outgoing) интерфейсы являются входящими (incoming).
Сток помогает клиенту слушать объект. Возможны варианты, когда одна воронка
подставляется для восприятия интерфейсов от нескольких разных СОМ-объектов (multicasting)
и когда один клиент предоставляет несколько воронок для восприятия интерфейсов
от одного объекта.
Каждая точка
соединения СОМ-объекта поддерживает интерфейс iConnect-ionPoint. С помощью другого
интерфейса — iConnectionPointContainer — объект рекламирует клиенту свои точки
связи. Клиент пользуется интерфейсом IConnectionPointContainer для получения
информации о наличии и количестве исходящих интерфейсов или, что то же самое,
точек соединения. Узнав о наличии IConnectionPoint, клиент использует его для
передачи объекту указателя на свой сток или нескольких указателей на несколько
стоков. Большинство, и Kraig Brockschmidt в том числе, отмечают, что все это
довольно сложно усвоить сразу, поэтому не переживайте, если потеряли нить рассуждений
в данной информации. Постепенно все уляжется.
Надо отметить,
что в этой части СОМ используется наибольшее число жаргонных слов. Попробуем
с их помощью коротко описать механизм, а также сценарий общения между клиентом
и С О М-объектом при задействовании исходящих интерфейсов. Сначала объект беспомощен
и не может сказать что-либо клиенту. Инициатива должна быть проявлена клиентом
— контейнером СОМ-объекта. Он обычным путем запрашивает у сервера указатель
на интерфейс IConnectionPointContainer, затем с помощью методов этого интерфейса
(EnumConnectionPoints, FindConnectionPoint) получает указатель на интерфейс
iConnectionPoint. Далее клиент использует метод Advise последнего интерфейса
для того, чтобы передать объекту указатель на свой сток — воронку для слушания
или слива сообщений. Начиная с этого момента объект имеет возможность разговаривать,
так как он имеет воронку или указатель на интерфейс посредника в виде sink.
Заставить замолчать объект может опять же клиент. Для этого он пользуется методом
Unadvise интерфейса IConnectionPoint.
Излишняя сложность
всей конструкции объясняется соображениями расширяемости (extensibility). Соединяемые
объекты могут усложняться независимо от точек соединения, а точки связи могут
развиваться, не принося тревог соединяемым объектам. Меня подобный довод не
убедил, но мы должны жить в этом мире, каков бы он ни был.
Карта
сообщений
Карта сообщений,
которая должна вызвать у вас ассоциацию с картой сообщений MFC, содержит незнакомый
макрос CHAIN_MSG_MAP. Он перенаправляет необработанные сообщения в карту сообщений
базового класса. Дело в том, что ATL допускает существование альтернативных
карт сообщений. Они определяются макросами ALT_MSG_MAP. Тогда надо использовать
макрос CHAIN_ MSG_MAP_ALT. Мы не будем обсуждать эту тему более подробно. Следующий
макрос — DEFAULT_ REFLECTION_HANDLER — обеспечивает обработчик по умолчанию
(в виде DefWindowProc) для дочерних окон элемента ActiveX, которые получают
отражаемое (reflected) сообщение, но не обрабатывают его.
Интерфейс
ISupportsErrorlnfо
Поддержка этого
интерфейса проста. В методе interfaceSupportsErrorinfo имеется статический массив
а г г, в котором хранятся адреса идентификаторов вновь создаваемых интерфейсов,
пока он у нас один HD_iOpenGL. В этом же методе осуществляется пробег по всему
массиву индексов и вызов функции inlinelsEqualGUio, которая пока не документирована,
но ее смысл может быть выведен из ее имени.
Интерфейс
IViewObjectEx
Этот интерфейс
является расширением интерфейса iviewobject2. Он поддерживает обработку объектов
непрямоугольной формы. Например, их улучшенную (flicker-free — не моргающую)
перерисовку, проверку попадания курсора внутрь объекта, изменение размеров и
полу прозрачность объектов. Моргание при перерисовке возникает из-за того, что
перед ней стирается все содержимое окна. Бороться с этим можно, например, так:
рисовать в bitmap (растровый рисунок), не связанный с экраном, а затем копировать
весь bitmap на экран одной операцией. Нас эта проблема не волнует, так как мы
будем использовать возможности OpenGL. Видимо, можно отказаться от услуг этого
интерфейса при оформлении заказа у мастера ATL. Макрос DECLARE_VIEW_STATUS задает
флаги прозрачности объекта, определенные в структуре VIEWSTATUS. По умолчанию
предложен набор из двух неразлучных флагов:
VIEWSTATUS_SOLIDBKGND
— использовать сплошной фон для окна в отличие от фона, основанного на узорной
кисти (brush pattern);
VIEWSTATUS_OPAQUE — объект
не содержит прозрачных частей, то есть полностью непрозрачен.
Макрос DECLARE_PROTECT_FINAL_CONSTRUCT
защищает объект от удаления в случае, если внутренний (агрегированный) объект
обнулит счетчик ссылок на наш объект. Метод CGomObjectRootEx: : FinalConstruct
позволяет создать агрегированный объект с помощью функции CoCreatelnstance.
Мы не будем пользоваться этой возможностью.
Карта
объектов
В аналогичном
проекте, созданном в рамках Visual Studio б, вы могли видеть карту объектов
ов JECT_MAP, которая обеспечивает поддержку регистрации, инициализации и создания
объектов. Карта объектов имеет привычную структуру:
BEGIN_OBJECT_MAP
OBJECT_ENTRY(CLSID_MyClass,
MyClass)
END_OBJECT_MAP()
где макрос
ов JECT_ENTRY вводит внутренний механизм отображений (тар) идентификаторов классов
В их имена. При вызове функции CComModule; :RegisterServer она вносит в реестр
записи, соответствующие каждому элементу в карте объектов. Здесь в рамках Studio.Net,
вы видите другой макрос — OBJECT_ENTRY_AUTO, выполняющий сходную функцию, но
при этом не нуждается в обрамлении из операторных скобок.
Введение
обработчиков сообщений Windows
Наш объект,
будучи активизирован в рамках окна контейнера, будет реагировать на сообщения
Windows. Он должен управляться мышью, поддерживать вращение с помощью таймера,
устанавливать нужный формат при создании своего окна и т. д. Введите в класс
copenGL способность реагировать на следующие сообщения:
WM_ERASEBKGND,
WM_LBUTTONDOWN, WM_RBUTTONDOWN, WM_LBUTTONUP, WM_RBUTTONUP, WM_MOUSEMOVE, WM_CREATE,
WM_DESTROY, WM_SIZE, WM_TIMER.
Для этого:
Поставьте курсор на строку
с именем класса COpenGL в окне ClassView и дайте команду Properties из контекстного
меню.
Нажмите кнопку Messages
на панели инструментов окна Properties.
Для того чтобы после
введения обработчика окно свойств не убегало, переведите его в режим Floating
и оттащите в сторону. В окне Class View должен быть выбран класс COpenGL
По очереди для всех перечисленных
сообщений укажите действие <Add> в правом столбце таблицы Properties.
Обработчик
сообщения OnEraseBkgnd вызывается операционной системой в те моменты, когда
фон окна должен быть стерт, например при изменении размеров окна. Родительская
версия этой функции или обработка по умолчанию использует для стирания (закрашивания)
кисть, указанную в структуре WNDCLASS при ее регистрации. Если надо отменить
стирание фона, то наша версия функции обработки должна установить специальный
флаг, который говорит о том, что сообщение обработано, иначе окно останется
помеченным как нуждающееся в стирании фона. Введите в файл реализации класса
COpenGL код обработки сообщения:
LRESULT
COpenGL::OnEraseBkgnd(UINT /*uMsg*/, WPARAM
/*wParam*/,
LPARAM /*lParam*/, BOOL& bHandled)
{
//======
Устанавливаем флаг завершения обработки
bHandled
= TRUE;
return
0;
}
Отметьте, что
прототип функции обработки отличается от того, который принят в MFC. Там он
имеет вид af x_msg BOOL OnEraseBkgnd(CDC* pDC); и определен в классе CWnd. Наш
класс COpenGL среди своих многочисленных предков имеет класс CComControl, который
происходит от класса CWindowlmpl, а тот, в свою очередь, является потомком класса
cwindow. Последний выполняет в ATL ту же роль, что и класс cwnd в MFC, но не
несет с собой бремени наследования от CObject. Это в основном и ускоряет функционирование
ATL-приложений.
Примечание
В заготовке тела функций
обработки все параметры закомментированы. Это сделано для того, чтобы упростить
работу компилятору, так как далеко не все параметры задействованы постоянно.
Если параметр необходимее его нужно сделать видимым для компилятора, убрав
знаки комментария. Сделайте это для параметра bHandled.
Теперь введите
в класс обработчик сообщения WM_CREATE и заполните его кодами, которые готовят
окно и устанавливают некоторые параметры OpenGL:
LRESULT
COpenGL::OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/,'LPARAM /*lParam*/, BOOL&
bHandled)
//=======
Описатель формата окна OpenGL
PIXELFORMATDESCRIPTOR
pfd =
{
sizeof(PIXELFORMATDESCRIPTOR),
//
Размер структуры
1,
//
Номер версии
PFD_DRAW_TO_WINDOW
|
//
Поддержка
GDI
PFD_SUPPORT_OPENGL |
//
Поддержка OpenGL
PFD_DOUBLEBUFFER,
//
Двойная буферизация
PFD_TYPE_RGBA,
//
Формат RGBA, не палитра
24,
//
Количество плоскостей
//
в каждом буфере цвета
24,
0,
//
Для компонента Red
24,
0,
//
Для компонента Green
24,
0,
//
Для компонента Blue
24,
0,
//
Для компонента Alpha
0,
//
Количество плоскостей
//
буфера Accumulation
0,
//
То же для компонента Red
0,
//
для компонента Green
0,
//
для компонента Blue
0,
// для компонента Alpha
32,
// Глубина Z-буфера
0,
// Глубина буфера Stencil
0,
// Глубина буфера Auxiliary
0,
// Теперь игнорируется
0,
// Количество плоскостей
0,
// Теперь игнорируется
0,
// Цвет прозрачной маски
0
// Теперь игнорируется
};
//
Добываем дежурный контекст и просим выбрать ближайший
m_hdc
= GetDCO ;
int
iD = ChoosePixelFormat(m_hdc, &pfd) ;
if
( !ID )
{
ATLASSERT(FALSE);
return
-1;
}
//======
Пытаемся установить этот формат
if
( ISetPixelFormat (m_hdc, iD, &pfd))
{
ATLASSERT(FALSE);
return
-1;
}
//======
Пытаемся создать контекст передачи OpenGL
if
( !(m_hRC = wglCreateContext (m_hdc)))
{
ATLASSERT(FALSE);
return
-1;
}
//======
Пытаемся выбрать его в качестве текущего
if
( !wglMakeCurrent (m_hdc, m_hRC))
{
ATLASSERT(FALSE);
return
-1;
}
//======
Теперь можно посылать команды OpenGL
glEnable
(GL_LIGHTING) ;
//
Будет освещение
glEnable
(GL_LIGHTO) ;
//
Только 1 источник
glEnable
(GL_DEPTH_TEST) ;
//
Учитывать глубину (ось Z)
//======
Учитывать цвет материала поверхности
glEnable
(GL_COLOR_MATERIAL) ;
//======
Устанавливаем цвет фона
SetBkColor
() ;
bHandled
= TRUE;
return
0;
}
Класс copenGL
должен реагировать на сообщение WM_SIZE и корректировать видимый объем сцены.
Мы будем использовать режим просмотра с учетом перспективы. Его определяет функция
gluPerspective. Введите в класс copenGL обработку WM_SIZE и вставьте в нее следующие
коды:
LRESULT
COpenGL: :OnSize(UINT /*uMsg*/, WPARAM /*wParam*/,
LPARAM
IParam, BOOL& bHandled)
{
//
Распаковываем длинный параметр и узнаем размеры окна
UINT
сх = LOWORD ( IParam) , су = HIWORD (IParam) ;
//======
Вычисляем максимальные диспропорции окна
double
dAspect = cx<=cy ? double (су) /сх
:
double (сх) /су;
//====
Задаем тип текущей матрицы (матрица проекции)
glMatrixMode
(GL_PROJECTION) ;
//======
Приравниваем ее к единичной диагональной
glLoadldentity
() ;
//==
Параметры перспективы (45 градусов - угол обзора)
gluPerspective
(45., dAspect, 1., 10000.);
glViewport
(0, 0, сх, су); DrawScene () ;
bHandled
= TRUE;
return
0;
}
Функция glViewport,
как вы помните, задает прямоугольник просмотра. При закрытии окна внедренного
объекта необходимо освободить память, занимаемую контекстом передачи, и отказаться
от услуг таймера, с помощью которого мы будем производить анимацию вращения
изображения. Введите в класс обработчик сообщения WM_DESTROY и измените ее стартовый
код, как показано ниже:
LRESULT
COpenGL: :OnDestroy (UINT /*uMsg*/, WPARAM
/*wParam*/,
LPARAM /*lParam*/, BOOL& bHandled)
{
KillTimer(l);
if
(m_hRC)
{
wglDeleteContext(m_hRC);
m_hRC = 0;
}
bHandled
= TRUE;
return
0;
}
Инициализация
переменных
В конструктор
класса вставьте код установки начальных значений переменных, с помощью которых
пользователь сможет управлять сценой 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,vy
fvz);
//
Ветвь создания несвязанных четырехугольников
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, либо с помощью страницы свойств, которую мы вскоре создадим. В любом
случае, эти методы должны либо получить параметр настройки изображения и перерисовать
его с учетом настройки, либо вернуть текущее состояние запрашиваемого параметра
настройки:

Рис. 9.1.
Окно ActiveX элемента, внедренного в окно тестового контейнера
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.
Установите фокус на элемент
ATLGL в дереве Solution Explorer и в контекстном меню выберите команду Add
> Add Class, при этом важно, чтобы фокус стоял на имени проекта ATLGL
В окне диалога Add Class
выберите категорию ATL, шаблон ATL Property Page и нажмите кнопку Open.
В окне мастера ATL Property
Page выберите вкладку Names и в поле Short Name введите PropDlg.
Перейдите на вкладку
Attributes и просмотрите допустимые установки, ничего в них не меняя.
Перейдите на вкладку
Strings и в поле Title введите имя страницы Light, которое будет обозначено
на вкладке (page tab). В поле Doc String введите строку Graphics Properties.
Нажмите кнопку 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) и
вам их вставлять не надо, необходимо сконструировать только облик самой страницы.


Рис. 9.2.
Вид новой вставки в блоке страниц свойств элемента ActiveX
На рисунке
показано окно диалога в активном состоянии, но вам еще предстоит поработать,
чтобы довести его до этого вида. Здесь очень важно не торопиться и быть внимательным.
Опыт преподавания в MS Authorized Educational Center (www.Avalon.ru) подтверждает,
что большая часть ошибок вносится на стадии работы с ресурсами. Визуальные редакторы
расслабляют внимание, и ошибки появляются там, где вы меньше всего их ждете.
В основных
чертах окно имеет тот же облик, что и окно диалога по управлению освещением
сцены, разработанное ранее (в MFC проекте). Но здесь есть два новых элемента,
функциональность которых ранее была спрятана в командах меню. Так как в рамках
этого проекта мы не имеем меню, то нам пришлось использовать элементы управления,
сосредоточенные в нижней части окна диалоговой вставки. Во-первых, не забудьте,
что справа от каждого ползунка вы должны расположить элемент типа static Text,
в окне которого будет отражено текущее положение движка в числовой форме.
Кнопка Выбор
файла, как и ранее, позволяет пользователю открыть файловый диалог и выбрать
файл с данными для нового изображения. Выпадающий список Заполнение позволяет
выбрать режим изображения полигонов (GL_FILL, GL_POINT или GL_LINE), а кнопка
Quads/Strip изменяет режим использования примитивов при создании поверхности.
Идентификаторы элементов управления мы сведем в табл. 9.1.
Таблица
9.1. Идентификаторы элементов управления
Ползунок Общая
в группе Освещенность
Ползунок Рассеянная
в группе Освещенность
Ползунок Отраженная
в группе Освещенность
Text справа от
Общая в группе Освещенность
Text справа от
Рассеянная в группе Освещенность
Text справа от
Отраженная в группе Освещенность
Ползунок Общая
в группе Материал
Ползунок Рассеянная
в группе Материал
Ползунок Отраженная
в группе Материал
Text справа от
Общая в группе Материал
Text справа от
Рассеянная в группе Материал
Text справа от
Отраженная в группе Материал
Text справа от
Блестскость
Выпадающий список
Заполнение