Требования 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=(constCPoint3D& 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:
//=====
Переменные, необходимые
для
реализации
интерфейса
OLE COLOR
m clrFillColor;
//
Цвет
фона окна
int
m LightParamfll]
;
//
Параметры
освещения
int
m xPos,
m yPos;
//
Текущая
позиция мыши
HGLRC
m hRC;
//
Контекст
OpenGL
HDC
m hdc;
//
Контекст
Windows
GLfloat
m AngleX;
//
Угол
поворота вокруг оси X
GLfloat
m AngleY;
//
Угол
поворота вокруг оси Y
GLfloat
m AngleView;
//
Угол
перспективы
GLfloat
m fRangeX;
//
Размер
объекта вдоль X
GLfloat
m fRangeY;
//
Размер
объекта вдоль Y
GLfloat
m fRangeZ;
//
Размер
объекта вдоль Z
GLfloat
m dx;
//
Квант
смещения вдоль X
GLfloat
m dy;
//
Квант
смещения вдоль Y
GLfloat
m xTrans;
//
Смещение
вдоль X
GLfloat
m yTrans;
//
Смещение
вдоль Y
GLfloat
m zTrans;
//
Смещение
вдоль Z
GLenum
m FillMode;
//
Режим
заполнения полигонов
bool
m_bCaptured;
//
Признак
захвата мыши
bool
m bRightButton;
//
Флаг
правой кнопки мыши
bool
m bQuad;
//
Флаг
использования GL QUAD
UINT
m xSize;
//
Текущий
размер окна вдоль X
UINT
m zSize;
//
Текущий
размер окна вдоль 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,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, либо с помощью страницы свойств, которую мы вскоре создадим. В любом
случае, эти методы должны либо получить параметр настройки изображения и перерисовать
его с учетом настройки, либо вернуть текущее состояние запрашиваемого параметра
настройки:
Рис. 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. Идентификаторы элементов управления
Элемент
Идентификатор
/ Диалог
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.
Поставьте фокус на элемент
управления, идентификатор которого вас не устраивает, и перейдите в окно Properties.
В конец строки с идентификатором
добавьте текст вида «=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, то есть цепочку вызовов функций. Для этого:
Дайте команду Debug
> Windows > Call Stack (или Alt+7).
Внедрите это окно, если
необходимо, в блок окон отладчика (внизу экрана).
Убедитесь, что вызов
произошел из функции DialogРгос одного из базовых классов, точнее шаблонов
классов, CDialoglmplBaseT.
Этот опыт иллюстрирует
тот факт, что все необычно в мире ATL. Этот мир устроен совсем не так, как MFC.
Шаблоны классов дают удивительную гибкость всей конструкции, способность приспосабливаться
и подстраиваться. Теперь рассмотрим вторую, весьма вероятную, ошибку. Секцию
protected в классе CPropDlg следует правильно разместить (странно, не правда
ли?). Лучше это сделать так, чтобы сразу за ней шло объявление какой-либо из
существующих секций public. Если поместить ее, например, перед макросом
DECLARE_REGISTRY_RESOURCEID(IDR__PROPDLG)
то макрос окажется
безоружным против такой атаки, хотя по идее он должен сопротивляться
и даже не замечать наскоков подобного рода. Возможно, этот феномен исчезнет
в окончательной версии Studio.Net.
Сообщение
о прокрутке в окне
Сообщение WM_HSCROLL
приходит в окно диалога (читайте: объекту диалогового класса, связанного с окном)
всякий раз, как пользователь изменяет положение одного из ползунков, расположенных
на лице диалога. Это довольно удобно, так как мы можем в одной функции обработки
(onHScroll) отследить изменения, произошедшие в любом из 11 регуляторов. Введите
коды обработки этого сообщения, которые сходны с кодами, приведенными в приложении
на основе MFC, за исключением СОМ-специфики общения между классами CPropDlg
и COpenGL:
LRESULT
CPropDlg::OnHScroll(UINT /*uMsg*/, WPARAM wParam,
LPARAM
iParam, BOOL& /*bHandled*/)
{
//======
Информация о событии запакована в wParara
int
nCode = LOWORD(wParam), nPos = HIWORD(wParam), delta, newPos;
HWND
hwnd = (HWND) IParam;
//
Выясняем номер и идентификатор активного ползунка
UINT
nID;
int
num = GetSliderNum(hwnd, nID);
//======
Выясняем суть события switch
(nCode)
{ case
SB_THUMBTRACK: case
SBJTHUMBPOSITION:
m_Pos[num]
= nPos; break;
//======
Сдвиг до упора влево (клавиша Home) case
SB_LEFT:
delta
= -100; goto
New_Pos;
//======
Сдвиг до упора вправо (клавиша End) case
SB_RIGHT:
delta
= + 100; goto
New_Pos;
case
SB_LINELEFT:
//
И т.д.
delta
= -1; goto
New_Pos;
case
SB_LINERIGHT:
delta
= +1; goto
New_Pos;
case
SB_PAGELEFT:
delta
= -20; goto
New_Pos;
case
SB_PAGERIGHT:
delta
= +20; goto
New_Pos;
New_Pos:
newPos
= m_Pos[num] + delta;
m_Pos[num]
= newPos<0 ? 0
:
newPos>100 ? 100 : newPos; break; case
SB_ENDSCROLL: default: return
0;
}
//===
Готовим текстовое выражение позиции ползунка char
s[8];
sprintf
(s,"%d",m_Pos[num]);
SetDlgltemText(nID,
(LPCTSTR)s);
//======
Цикл пробега по всем объектам типа PropDlg for
(UINT i = 0; i < m_nObjects; )
//======
Добываем интеофейсн:
//======
Добываем интерфейсный указатель
CComQIPtr<IOpenGL,
&IID_IOpenGL> p (m_ppUnk[i] ) ;
//======
Устанавливаем конкретный параметр if
FAILED (p->SetLightParam (num, m_Pos [num] ) )
ShowError();
return
0;
}
} return
0;
}
В данный момент
вы можете проверить функционирование регуляторов в суровых условиях СОМ. Они
должны работать.
Реакция
на выбор в окне выпадающего списка
Теперь введем
реакцию на выбор пользователем новой строки в окне выпадающего списка. Для этого
выполните следующие действия:
Откройте в окне редактора
Studio.Net шаблон окна диалога IDD_PROPDLG.
Поставьте фокус в окно
выпадающего списка IDC_FILLMODE и переведите фокус окно Properties.
Нажмите кнопку Control
Events, расположенную на инструментальной панели окна Properties.
Найдите строку с идентификатором
уведомляющего сообщения CBN_SELCHANGE и в ячейке справа выберите действие
<Add>, для того чтобы там появилось имя функции обработки OnSelchangeFillmode.
Перейдите в окно PropDlg.cpp
и введите следующие коды в заготовку функции OnSelchangeFillmode.
LRESULT
CPropDlg
::OnSelchangeFillmode(WORD/*wNotifyCode*/,
WORD /*wID*/,
HWND
hWndCtl, BOOL& bHandled)
{
//======
Цикл пробега по всем объектам типа PropDlg for
(UINT i = 0; i < m_nObjects; i++)
{
CComQIPtr<IOpenGL,
&IID_IOpenGL> p(m_ppUnk[i]);
//
Выясняем индекс строки, выбранной в окне списка
DWORD
sel = (DWORD)SendMessage(hWndCtl, CB_GETCURSEL,0,0);
//
Преобразуем индекс в режим отображения полигонов
sel
= sel==0 ? GL_POINT
:
sel==l ? GL_LINE : GL_FILL;
//======
Устанавливаем режим в классе COpenGL if
FAILED (p->SetFillMode(sel))
{
ShowError();
return
0;
}
}
bHandled
= TRUE;
return
0;
}
Обратите внимание
на то, что нам пришлось убирать два комментария, чтобы сделать видимым параметры
hWndCtl и bHandled.
Реакция
на нажатия кнопок
При создании
отклика на выбор режима изображения полигонов следует учесть попеременное изменение
текста и состояния кнопки. Поставьте курсор на кнопку IDC_QUADS и в окне Properties
нажмите кнопку Control Events. Затем найдите строку с идентификатором уведомляющего
сообщения BN_CLICKED и в ячейке справа выберите действие <Add>. Текст
в ячейке должен измениться и стать OnClickedQuads. Введите следующие коды в
заготовку функции:
LRESULT
CPropDlg::OnClickedQuads(WORD /*wNotifyCode*/,
WORD
/*wID*/, HWND /*hWndCtl*/, BOOL& bHandled)
{
//======
По всем объектам PropDlg for
(UINT i = 0; i < m_nObjects; i++)
{
//======
Добываем интерфейсный указатель
CComQIPtr<IOpenGL,
&IID_IOpenGL> p(m_ppUnk[i]) ;
//======
Переключаем режим
m_bQuad
= !m_bQuad;
//======
Устанавливаем текст на кнопке
SetDlgltemText(IDC_QUADS,
m_bQuad ? "Quads" : "Strip");
if
FAILED (p->SetQuad(m_bQuad))
{
ShowError();
return
0;
bHandled
= TRUE;
return
0;
}
Аналогичные,
но более простые действия следует произвести в реакции на нажатие кнопки Выбор
файла. Введите функцию для обработки этого события и вставьте в нее следующий
код:
LRESULT
CPropDlg: rOnCl'ickedFilename (WORD /*wNotif yCode*/,
WORD
/*wID*/, HWND /*hWndCtl*/, BOOL& bHandled)
{ for
(UINT i = 0; i < m_nObjects; i++)
{
CComQIPtr<IOpenGL,
&IID_IOpenGL> p (m_ppUnk [i] ) ;
//======
Вызываем функцию класса COpenGL if
FAILED (p->ReadData() )
{
ShowError
() ;
return
0 ;
}
bHandled
= TRUE;
return
0;
}
Постройте сервер
и проверьте работу страницы свойств. Попробуйте прочесть другой файл, например
тот, который был создан приложением, созданным в рамках MFC. Так как мы не изменяли
формат данных, записываемых в файл, то все старые файлы должны читаться.
Управление
объектом с помощью мыши
Алгоритм управления
ориентацией объекта с помощью мыши мы разработали ранее. Вы помните, что перемещение
курсора мыши при нажатой кнопке должно вращать изображение, причем горизонтальное
перемещение вращает его вокруг вертикальной оси Y, а вертикальное — вокруг горизонтальной
оси X. Если одновременно с мышью нажата клавиша Ctrl, то объект перемещается
(glTranslatef) вдоль осей X и Y. Наконец, с помощью правой кнопки изображение
перемещается вдоль оси Z, то есть приближается или отдаляется. Таймер помогает
нам в том, что продолжает вращение, если очередной квант перемещения мышью стал
выше порога чувствительности. Скорость вращения имеет два пространственных компонента,
которые пропорциональны разности двух последовательных во времени координат
курсора. Чем быстрее движется курсор при нажатой левой кнопке, тем большая разность
координат будет обнаружена в обработчике сообщения WM_MOUSEMOVE. Именно в этой
функции оценивается желаемая скорость вращения.
Описанный алгоритм
обеспечивает гибкое и довольно естественное управление ориентацией объекта,
но, как вы помните, он имеет недостаток, который проявляется, когда модуль угла
поворота вдоль первой из вращаемых (с помощью glRotate) осей, в нашем случае
— это ось X, превышает 90 градусов. Вам, читатель, я рекомендовал самостоятельно
решить эту проблему и устранить недостаток. Ниже приводится одно из возможных
решений. Если вы, читатель, найдете более изящное, буду рад получить его от
вас. Для начала следует ввести в состав класса COpenGL функцию нормировки углов
вращения, которая, учитывая периодичность процесса, ограничивает их так, чтобы
они не выходили из диапазона (-360°, 360°): void
COpenGL::LimitAngles()
{
//======
Нормирование углов поворота так,
//======
чтобы они были в диапазоне (-360°, +360°) while
(m_AngleX < -360.f)
m_AngleX
+= 360.f;
while
(m_AngleX > 360.f)
m_AngleX
-= 360.f;
while
(m_AngleY < -360.f)
m_AngleY
+= 360.f;
while
(m_AngleY > 360.f)
m_AngleY
-= 360.f;
}
Затем следует
вставить вызовы этой функции в те точки программы, где изменяются значения углов.
Кроме того, надо менять знак приращение m_dx, если абсолютная величина угла
m_AngleX попадает в диапазон (90°, 270°). Это надо делать при обработке сообщения
WM_MOUSEMOVE. Ниже приведена новая версия функции обработки этого сообщения,
а также сообщения WM_TIMER, в которое также следует ввести вызов функции нормировки:
LRESULT
COpenGL::OnMouseMove(UINT /*uMsg*/, WPARAM wParam, LPARAM IParam, BOOL&
bHandled)
{
//======
Если был захват if
(m_bCaptured)
{
//======
Вычисляем желаемую скорость вращения
short
xPos = (short)LOWORD(IParam);
short
yPos = (short)HIWORD(1Param);
m_dy
= float(yPos - m_yPos)/20.f;
m_dx
= float(xPos - m_xPos)/20.f;
//======
Если одновременно была нажата Ctrl, if
(wParam & MK_CONTROL)
{
//===
Изменяем коэффициенты сдвига изображения
m_xTrans
+= m_dx;
m_yTrans
-= m_dy;
} else
{
//======
Если была нажата правая кнопка if
(m_bRightButton)
//======
Усредняем величину сдвига
m_zTrans
+= (m_dx + m_dy)/2.f;
else
{
//======
Иначе, изменяем углы поворота
//======
Сначала нормируем оба угла
LiraitAngles();
//===
Затем вычисляем модуль одного из них
double
a = fabs(m_AngleX);
//
и изменяем знак приращения(если надо)
if
(90. < а && а < 270.) m_dx = -m_dx;
m_AngleX
+= m_dy;
m_AngleY
+= m_dx;
}
}
//
В любом случае запоминаем новое положение мыши
m_xPos
= xPos;
m_yPos
= yPos;
FireViewChange();
}
bHandled
= TRUE; return 0;
}
LRESULT
COpenGL: :OnTimer (UINT /*uMsg*/, WPARAM
/*wParam*/,
LPARAM /*lParam*/, BOOL& bHandled)
{
//======
Нормировка углов поворота
LimitAngles
() ;
//======
Увеличиваем эти углы
m_AngleX
+= m_dy; m_AngleY += m_dx;
//======
Просим перерисовать окно
FireViewChange();
bHandled
= TRUE;
return
0;
}
Ниже приведены
функции обработки других сообщений мыши. Они сходны с теми, которые мы разработали
для MFC-приложения, за исключением прототипов и возвращаемых значений. Начнем
с обработки нажатия левой кнопки. Оно, очевидно, должно всегда останавливать
таймер, запоминать факт нажатия кнопки и текущие координаты курсора мыши:
LRESULT
COpenGL::OnLButtonDown(UINT /*uMsg*/, WPARAM
/*wParam*/,
LPARAM IParam, BOOL& bHandled)
{
//======
Останавливаем таймер
KillTimer(1);
//======
Обнуляем кванты перемещения
m_dx
= O.f;
m_dy
= 0.f;
//======
Захватываем сообщения мыши,
//======
направляя их в свое окно
SetCapture();
//======
Запоминаем факт захвата
m_bCaptured
= true;
//======
Запоминаем координаты курсора
m_xPos
= (short)LOWORD(IParam);
m_yPos
= (short)HIWORD(IParam);
bHandled
= TRUE; return 0;
}
В обработчик
отпускания левой кнопки вводится анализ на необходимость продолжения вращения
с помощью таймера. В случае превышения порога чувствительности, следует запустить
таймер, который продолжает вращение, поддерживая текущее значение его скорости.
Любопытно, что в алгоритме нам не понадобился ни один их входных параметров
функции:
LRESULT
COpenGL::OnLButtonUp(UINT /*uMsg*/, WPARAM
/*wParam*/,
LPARAM /*lParam*/, BOOL& bHandled)
{
//======
Если был захват, if
(m_bCaptured)
{
//===
то анализируем желаемый квант перемещения
//===
на превышение порога чувствительности
if
(fabs(m_dx) > 0.5f || fabs(m_dy) > 0.5f)
//======
Включаем режим постоянного вращения
SetTimer(1,33)
;
else
//======
Выключаем режим постоянного вращения
KillTimer(1);
//======
Снимаем флаг захвата мыши
m_bCaptured
= false;
//======
Отпускаем сообщения мыши
ReleaseCapture();
}
bHandled
= TRUE;
return
0;
}
При нажатии
на правую кнопку выполняются те же действия, что и при нажатии на левую, но
дополнительно запоминается факт нажатия правой кнопки, с тем чтобы можно было
правильно интерпретировать последующие сообщения о перемещении указателя мыши
и вместо вращения изображения производить его сдвиг вдоль оси Z. Отметьте тот
факт, что мы должны убрать символы комментариев вокруг параметров:
LRESULT
COpenGL::OnRButtonDown(UINT uMsg, WPARAM wParam,
LPARAM
IParam, BOOL& bHandled)
{
//======
Запоминаем факт нажатия правой кнопки
m_bRightButton
= true;
//======
Воспроизводим реакцию на левую кнопку
OnLButtonDown(uMsg,
wParam, IParam, bHandled);
return
0;
}
Отпускание
правой кнопки просто отмечает факт прекращения перемещения вдоль оси Z и отпускает
сообщения мыши (ReleaseCapture), для того чтобы они могли правильно обрабатываться
другими окнами, в том числе и нашим окном-рамкой. Если этого не сделать, то
будет невозможно использоваться меню:
LRESULT
COpenGL::OnRButtonUp(UINT /*uMsg*/, WPARAM
/*wParam*/,
LPARAM /*lParam*/, BOOL& bHandled)
{
m_bRightButton
= false;
m_bCaptured
= false;
ReleaseCapture();
bHandled
= TRUE; return
0;
}
Запустите и
проверьте управляемость объекта. Введите коррективы чувствительности мыши. В
заключение отметим, что при выборе параметров заготовки ATL мы могли на вкладке
Miscellaneous (Разное) поднять не только флажок Insertable, но и windowed Only.
Это действие сэкономило бы те усилия, которые были потрачены на поиск неполадок,
вызванных отсутствием флага m bWindowOnly.
Visual Studio Net
Работа с Visual Studio.Net
Создание
контейнера на базе MFC
До сих пор
для отладки и демонстрации нашего ActiveX-элемента мы пользовались услугами
тестового контейнера ActiveX Control Test Container,который входит в состав
инструментов Studio.Net. Пришла пора показать, как с помощью библиотеки классов
MFC можно создать свой собственный простой контейнер, специализированный для
управления элементом OpenGL Class.
Создайте новый основанный
на диалоге MFC-проект и назовите его TestGL.
В окне вновь созданного
шаблона диалога IDD_TESTGL_DIALOG вызовите контекстное меню и выберите команду
Insert ActiveX Control.
В окне появившегося диалога
Insert ActiveX Control найдите строку OpenGL Class и нажмите ОК.
Вы должны увидеть рамку
нового элемента, размещенного Studio.Net в форме диалога. Элементу присвоен
идентификатор IDCJDPENGLI, который можно увидеть в окне Properties. Уберите
из него завершающую единицу.
Растяните окно нового
элемента так, чтобы оно занимало примерно 80% площади всего окна диалога (рис.
9.3).
Рис. 9.3.
Внедрение элемента ActiveX в окно диалогового приложения
В отличие от
Visual Studio б в конце этой процедуры в состав проекта (по умолчанию) не будет
включен новый класс-оболочка (wrapper class) под именем CGpenGL. Такой класс
необходим для дальнейшей работы с внедренным элементом ActiveX.
В документации
бета-версии Studio.Net я нашел лишь намек на то, что wrapper-класс может быть
создан с помощью ClassWizard. Однако мне не удалось добиться этого. Поэтому
мы создадим класс-оболочку вручную. Конечно, здесь я использую заготовку класса,
полученную в рамках Visual Studio 6. Она оказалась вполне работоспособной и
в новой Studio.Net. Будем надеяться, что в следующих версиях Studio.Net рассмотренный
процесс автоматического создания класса будет достаточно прозрачен.
Класс-оболочка
Обычно при
создании приложения-контейнера для элемента ActiveX придерживаются следующей
стратегии:
Вставляют уже зарегистрированный
элемент ActiveX в форму приложения контейнера, используя так называемую галерею
объектов (Gallery).
В одном из классов контейнера
определяют переменную того же типа, что и класс-оболочка для внедренного элемента.
Программируют поведение
элемента, управляя им с помощью этой переменной.
Первый шаг
этого алгоритма вы уже выполнили, теперь введите в состав проекта два новых
файла OpenGLh и OpenGLcpp, которые будут содержать коды класса-оболочки copenGL.
Вот содержимое файла заголовков (OpenGLh): #pragma
once
//===========
COpenGL wrapper class class
COpenGL : public CWnd
{ protected:
DECLARE_DYNCREATE(COpenGL) public:
//====
Метод для добывания CLSID нашего элемента
CLSID
const& GetClsidO
{ static
CLSID const clsid =
{
0x519d9ed8,
Oxbc4'6, 0x4367,
{
Ox9c, OxcO, 0x49, 0x81, 0x40, Oxf3, 0x94, 0x16 }
}; return
clsid;
} virtual
BOOL Create(LPCTSTR IpszClassName,
LPCTSTR
IpszWindowName, DWORD dwStyle,
const
RECT& rect, CWnd* pParentWnd, UINT nID, CCreateContext* pContext = NULL)
{ return
CreateControl(GetClsid(), IpszWindowName,
dwStyle,
rect, pParentWnd, nID)
}
BOOL
Create (LPCTSTR IpszWindowName, DWORD dwStyle, const
RECT& rect, CWnd* pParentWnd, UINT nID, CFile* pPersist = NULL,
BOOL
bStorage = FALSE, BSTR bstrLicKey = NULL)
{ return
CreateControl(GetClsidO, IpszWindowName, dwStyle, rect, pParentWnd, nID,
pPersist, bStorage, bstrLicKey);
}
//======
Методы, экспонируемые элементом ActiveX public: void
SetFillColor(unsigned long newValue); unsigned
long GetFillColor(); void
GetLightParams(long* pPos); void
SetLightParam(short Ip, long nPos); void
ReadData(); void
SetFillMode(DWORD mode); void
GetFillMode(DWORD* pMode); void
GetQuad(BOOL* bQuad); void
SetQuad(BOOL bQuad);
};
Самым важным
моментом в процедуре вставки класса является правильное задание CLSID того класса
OpenGL, который был зарегистрирован в операционной системе при создании DLL-сервера,
то есть нашего элемента ActiveX. He пытайтесь сравнивать те цифры, которые приведены
в книге, с теми, которые были приведены в ней же до этого момента, так как в
процессе отладки пришлось не раз менять как классы, так и целиком приложения.
Мне не хочется отслеживать эти жуткие номера. Если вы хотите вставить правильные
цифры, то должны взять их из вашей версии предыдущего приложения ATLGL.
Например, откройте файл ATLGL.IDL и возьмите оттуда CLSID для ко-класса OpenGL,
то есть найдите такой фрагмент этого файла:
[
uuid(519D9ED8-BC46-4367-9CCO-498140F39416), helpstring("OpenGL
Class") ]
coclass
OpenGL { [default]
interface IOpenGL; [default,
source] dispinterface _IOpenGLEvents;
};
И скопируйте
первую строку
uuid(519D9ED8-BC46-4367-9CCO-498140F39416),
но с вашими
цифрами и вставьте ее в качестве комментария в файл OpenGLh нового
проекта TestGL. Затем аккуратно, соблюдая формат, принятый для структуры CLSID,
перенесите только цифры в поля статической структуры clsid, которую вы видите
в методе GetClsid класса-оболочки. Цифры должны быть взяты из принесенной строки,
но их надо отформатировать (разбить) по-другому принципу. Например, для нашего
случая правильным будет такое тело метода GetClsid:
CLSID
const& GetClsid()
{
//
Следующая строка взята из файла ATLGL.IDL
//
519D9ED8-BC46-4367-9CCO-498140F39416 static
CLSID const clsid =
{
//========
Эти цифры взяты из файла ATLGL.IDL
0x519d9ed8,
0xbc46, 0x4367,
{
0х9с, 0xc0, 0x49, 0x81, 0x40, 0xf3, 0x94, 0x16 } ) ; return
clsid;
}
Кроме этого
важного фрагмента в новом классе объявлены два совмещенных метода Create, каждый
из которых умеет создавать окно внедренного элемента ActiveX с учетом особенностей
стиля окна (см. справку по CWnd: :CreateControl). Затем в классе-оболочке должны
быть представлены суррогаты всех методов, экспонируемых классом OpenGL COM DLL-сервера
ATLGL.DLL. В том, что вы не полностью приводите тела методов сервера, иначе
это был бы абсурд, хотя и так близко к этому, можно убедиться, просмотрев на
редкость унылые коды реализации класса-оболочки, которые необходимо вставить
в файл OpenGLcpp. Утешает мысль, что в исправной Studio.Net эти коды не придется
создавать и редактировать вручную: #include
"stdafx.h" #include
"opengl.h"
IMPLEMENT_DYNCREATE(COpenGL,
CWnd)
//======
Стандартное свойство реализовано
//======
в виде пары методов Get/Set void
COpenGL::SetFillColor(unsigned long newValue)
{
static
BYTE parms[] =
VTS_I4;
InvokeHelper(0xfffffe02, DISPATCH_PROPERTYPUT,VT_EMPTY,
NULL,
parms, newValue);
}
//======
Стандартное свойство unsigned
long COpenGL::GetFillColor0 { unsigned
long result;
InvokeHelper
(Oxfffffe02, DISPATCH_PROPERTYGET, VT_I4, (void4)&result,
NULL); return
result;
}
//======
Наши методы сервера void
COpenGL::GetLightParams(long* pPos)
{ static
BYTE parms[] = VTS_PI4;
InvokeHelper
(Oxl, DISPATCH_METHOD, VT_EMPTY, NULL,
parms,
pPos);
} void
COpenGL: : SetLightParam (short lp, long nPos)
{ static
BYTE parms [ ] = VTS 12 VTS 14;
InvokeHelper{0x2,
DISPATCH_METHOD, VT_EMPTY, NULL,
parms,
lp, nPos);
} void
COpenGL::ReadData()
InvokeHelper(0x3,
DISPATCH_METHOD, VT_EMPTY, 0, 0) ; void
COpenGL::GetFillMode(DWORD* pMode) static
BYTE jparms[] =
VTS_PI4;
InvokeHelper (0x4, DISPATCH_METHOD, VT_EMPTY, NULL,
parms,
pMode);
} void
COpenGL::SetFillMode(DWORD nMode) static
BYTE parms[] =
VTS_I4;
InvokeHelper(0x5,
DISPATCH_METHOD, VT_EMPTY, NULL, parms, nMode); void
COpenGL::GetQuad(BOOL* bQuad) static
BYTE parms[] =
VTS_PI4;
InvokeHelper(0x6,
DISPATCH_METHOD, VT_EMPTY, NULL, parms, bQuad); void
COpenGL::SetQuad(BOOL bQuad) static
BYTE parms[] =
VTS_I4;
InvokeHelper
(0x7, DISPATCH_METHOD, VT_EMPTY, NULL, parms, bQuad);
}
Затем подключите
оба новых файла к проекту Project > Add Existing Item.
Управление
с помощью объекта класса-оболочки
Для управления
внедренным элементом ActiveX надо ввести в существующий диалоговый класс CTestGLDlg
объект (переменную типа) класса-оболочки. Этот шаг тоже автоматизирован в Studio.Net,
так как введение объекта влечет сразу несколько строк изменения кода.
Поставьте фокус на окно
внедренного элемента IDC_OPENGL в форме диалога и вызовите контекстное меню.
В меню выберите команду
Variable, которая запустит мастер Add Member Variable Wizard.
Установите флажок Control
Variable и задайте в полях диалоговой страницы мастера следующие значения:
Access — public, Variable type — COpenGL, Variable name — * m_Ctrl, Control
ID - IDC_OPENGL
Обратите внимание на
то, что в.поле Control type уже выбран тип элемента OCX, и нажмите кнопку
Finish.
Результатом
работы мастера являются следующие строки программы:
объявление переменной
COpenGL m_ctrl; в файле заголовков TestGLDlg.h;
вызов функции DDX_Control(pDX,
IDC_OPENGL, m_ctrl), связывающей элемент управления в окне диалога с переменной
m_ctrl. Этот вызов вы найдете в теле функции CTestGLDlg::DoDataExchange;
Для обеспечения
видимости вставьте в начало файла TestGLDlg.h директиву: #include
"opengl.h"
В конец файла
Stdafx.h вставьте директивы подключения заголовков библиотеки OpenGL:
#include
<gl/gl.h>
//
Будем пользоваться OpenGL
#include
<gl/glu.h>
Теперь следует
поместить в окно диалога элементы управления. Здесь мы не будем пользоваться
страницами свойств элемента, созданными нами в рамках предыдущего проекта. Вместо
этого мы покажем, как можно управлять внедренным элементом ActiveX с помощью
объекта m_ctrl. Перейдите в окно диалогового редактора и придайте окну диалога
IDD_TESTGL_DIALOG.
Идентификаторы
для элементов управления можно задать так, как показано в табл. 9.2.
Для кнопки
Quads установите свойство Group в положение True, а для кнопки Strips — в False.
Обе они должны иметь свойство Auto в состоянии True. Важно еще то, что числовые
значения их идентификаторов должны следовать по порядку. Для кнопки Data File
установите свойство DefaultButton. Для выпадающего списка снимите свойство Sort
(сделайте его False) и слегка растяните вниз его окно в открытом состоянии,
для этого сначала нажмите кнопку раскрывания. Для ползунка вы можете установить
свойство Point в положение Top/Left. Обратите внимание на тот факт, что в режиме
дизайна вы можете открыть с помощью правой кнопки мыши диалог со страницами
свойств для элемента IDC_OPENGL, одну из которых мы создавали в предыдущем проекте.
Теперь с помощью Studio.Net введите в диалоговый класс обработчики следующих
событий:
OnClickedFilename —
нажата кнопка IDC_FILENAME,
OnCiickedBkcir — нажата
кнопка IDC_BKCLR,
OnSelchangeFill — изменился
выбор в списке IDC_FILL,
OnClickedQuads — нажата
кнопка IDC_QUADS,
OnHScroll — изменилась
позиция ползунка IDC_XPOS,
OnClickedStrips — нажата
кнопка IDC_STRIPS.
Ниже мы приведем
тела этих функций, а сейчас отметим, что все они пользуются услугами класса-оболочки
для прямого вызова методов СОМ-сервера. Однако, как вы могли заключить из рассмотрения
кодов класса COpenGL, на самом деле вызов будет происходить с помощью интерфейса
IDispatch, а точнее его метода Invoke. Функция cwnd: : invokeHelper, вызов которой
вы видите во всех методах COpenGL, преобразует параметры к типу VARIANTARG,
а затем вызывает функцию Invoke. Если происходит отказ, то Invoke выбрасывает
исключение.
В диалоговом
классе мы попутно произвели упрощения, которые связаны с удалением ненужных
функций OnPaint и OnQueryDragicon. Эти изменения обсуждались при разработке
приложения Look. Во избежание недоразумений, которые могут возникнуть в связи
с многочисленным ручным редактированием, приведем коды как декларации, так и
реализации класса CTestGLDlg:
//===
Декларация диалогового класса (Файл TestGLDlg.h)
#include
"opengl.h"
#pragma
once
class
CTestGLDlg : public CDialog
{ public:
CTestGLDlg(CWnd*
p = NULL); enum
{
IDD
= IDD_TESTGL_DIALOG
};
//=======
Объект класса-оболочки
COpenGL
m_Ctrl;
//=======
Запоминаем способ изображения
BOOL
m_bQuads;
//=======
Реакции на регуляторы в окне диалога void
OnSelchangeFill(void); void
OnClickedFilename(void);
afx_msg
void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); void
OnCiickedBkcir(void); void
OnClickedQuads(void);
void
OnClickedStrips(void); protected: virtual
void
DoDataExchange(CDataExchange* pDX) ;
virtual
BOOL OnlnitDialog();
afx_msg
void OnSysCommand(UINT nID, LPARAM IParam);
DECLARE_MESSAGE_MAP()
};
В файл реализации
методов класса мы кроме функций обработки сообщений от элементов управления
вставили код начальной установки этих элементов. Для этой цели нам опять понадобилась
связь с сервером, которую обеспечивает объект m_ctrl класса-оболочки. Характерным
моментом является то, что обрабатываем событие WM_HSCROLL, которое поступает
окну диалога, вместо того чтобы обработать уведомляющее событие NM_RELEASEDCAPTURE,
которое идет от элемента типа Slider Control. Такая тактика позволяет реагировать
на управление ползунком клавишами, а не только мышью: #include
"stdafx.h"
#include
"TestGL.h" #include
"TestGLDlg.h" #ifdef
_DEBUG #define
new DEBUG_NEW #undef
THIS_FILE static
char THIS_FILE[] = _FILE_; #endif
//======
Пустое тело конструктора
CTestGLDlg::CTestGLDlg(CWnd*
p) : CDialog(CTestGLDlg::IDD, p){} void
CTestGLDlg::DoDataExchange(CDataExchange* pDX) {
//======
Связывание переменной с элементом
DDX_Control(pDX,
IDCJDPENGL, m_Ctrl);
CDialog::DoDataExchange(pDX);
}
//======
Здесь мы убрали ON_WM_PAINT и т. д.
BEGIN_MESSAGE_MAP(CTestGLDlg,
CDialog) ON_WM_SYSCOMMAND()
//
}
}
AFX_MSG_MAP
ON_CBN_SELCHANGE(IDC_FILL,
OnSelchangeFill)
ON_BN_CLICKED(IDC_FILENAME,
OnClickedFilename)
ON_WM_HSCROLL()
ON_BN_CLICKED(IDC_BKCLR,
OnClickedBkclr)
ON_BN_CLICKED(IDC_QUADS,
OnClickedQuads)
ON_BN_CLICKED(IDC_STRIPS,
OnClickedStrips)
END_MESSAGE_MAP()
//=====
CTestGLDlg message handlers
BOOL
CTestGLDlg::OnInitDialog()
{
//======
Добываем адрес меню управления окном
CMenu*
pSysMenu = GetSystemMenu(FALSE); if
(pSysMenu)
{
//======
Добавляем команду About
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING,
IDM_ABOUTBOX,"About...");
}
//======
Загружаем стандартный значок
HICON
hMylcon = ::LoadIcon(0,(char*)IDI_WINLOGO);
Setlcon(hMylcon,
TRUE); // Set big icon Setlcon(hMylcon, FALSE);
//
Set small icon
CDialog::OnInitDialog();
//======
Начальная установка элементов
CComboBox
*pBox = (CComboBox*)GetDlgltem(IDC_FILL);
pBox->AddString("Points");
pBox->AddString("Lines");
pBox->AddString("Fill");
pBox->SetCurSel (2);
//====
Выясняем состояние режима изображения полигонов
m_Ctrl.GetQuad(&m_bQuads);
WPARAM
w = m_bQuads ? BST_CHECKED : BST_UNCHECKED;
//=====
Устанавливаем состояние переключателя
GetDlgltem(IDC_QUADS)->SendMessage(BM_SETCHECK,
w, 0);
w
= m_bQuads ? BST_UNCHECKED : BST_CHECKED;
GetDlgltem(IDC_STRIPS)->SendMessage(BM_SETCHECK,
w, 0); return
TRUE;
} void
CTestGLDlg::OnSysCommand(UINT nID, LPARAM iParam)
{ if
((nID S OxFFFO) == IDM_ABOUTBOX)
{
CDialog(IDD_ABOUTBOX).DoModal();
} else
{
CDialog::OnSysCommand(nID,
IParam);
}
}
//======
Выбор из списка типа Combo-box void
CTestGLDlg::OnSelchangeFill(void) "'*
{
DWORD
sel = ((CComboBox*)GetDlgltem(IDC_FILL))->GetCurSel();
sel
= sel==0 ? GL_POINT : sel==l ? GL_LINE
:
GL_FILL;
m_Ctrl.SetFillMode(sel);
}
//====
Нажатие на кнопку запуска файлового диалога
void
CTestGLDlg::OnClickedFilename(void)
{
m_Ctrl.ReadData();
}
//======
Реакция на сдвиг ползунка
void
CTestGLDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
//======
Выясняем текущую позицию, которая не во
//======
всех случаях отражена в параметре nPos
nPos
= ((CSliderCtrl*)GetDlgItem(IDC_XPOS))->GetPos() ;
m_Ctrl.SetLightParam
(0, nPos);
}
//======
Запускаем стандартный диалог
void
CTestGLDlg::OnClickedBkclr(void)
{
DWORD
clr = m_Ctrl.GetFillColor() ;
CColorDialog
dig (clr);
dig.m_cc.Flags
|= CC_FULLOPEN; if
(dlg.DoModal()==IDOK)
{
m_Ctrl.SetFillColor(dlg.m_cc.rgbResult);
}
}
//======
Запоминаем текущее состояние и
//======
вызываем метод сервера
void
CTestGLDlg::OnClickedQuads(void)
{
m_Ctrl.SetQuad(m_bQuads
= TRUE);
}
void
CTestGLDlg::OnClickedStrips(void)
{
m_Ctrl.SetQuad(m_bQuads
= FALSE);
}
В настоящий
момент вы можете запустить приложение, которое должно найти и запустить DLL-сервер
ATLGL, генерирующий изображение по умолчанию и демонстрирующий его в окне внедренного
элемента типа ActiveX. Сервер должен достаточно быстро реагировать на изменение
регулировок органов управления клиентского приложения.
Подведем итог.
В этом уроке мы научились:
вносить функциональность
окна OpenGL в окно, управляемое ATL-классом CWindow;
добавлять с помощью Studio.Net
новые методы в интерфейс, представляемый ко-классом;
учитывать особенности
обработки сообщений Windows в рамках ATL;
управлять контекстом
передачи OpenGL, связанным с окном внедренного СОМ-объекта;
создавать приложение-контейнер
на базе MFC и пользоваться услугами класса-оболочки для управления СОМ-объектом.