Трехмерная графика в проекте ATL

В этом уроке мы продолжим разработку 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). Вы также помните, что подготовку контекста надо рассматривать как некий обязательный ритуал, в котором порядок действий определен. Повторим его:

Чтобы использовать функции библиотеки OpenGL, надо подключить их к проекту. На этапе компоновки они будут интегрированы в коды СОМ-сервера.

  1. В окне Solution Explorer поставьте фокус на строку с именем проекта ATLGL и нажмите кнопку Properties, которая расположена на панели инструментов этого окна.
  2. В левом окне диалога ATLGL Property Pages найдите и выберите ветвь дерева Linker.
  3. В раскрывшемся поддереве выберите ветвь Input и перейдите в строку Additional Inputs в таблице правого окна.
  4. Поставьте фокус во вторую колонку этой строки и в конец существующего текста ячейки добавьте, не стирая содержимое ячейки, имена подключаемых библиотек OPENGL32.LIB GLU32.LIB, не забыв о разделяющих пробелах. Нажмите ОК.
  5. В конец файла библиотечных заголовков 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, которая определяет действующие параметры освещения.

  1. Поставьте фокус на строку с именем интерфейса lOpenGL в окне CLassView и вызовите контекстное меню.
  2. Выберите команду Add > Add Method В окне мастера Add Method Wizard введите в поле Method Name имя метода GetLightParams. В поле Parameter Name введите имя параметра pPos, в поле Parameter Type: — тип параметра int*, задайте атрибут параметра, установив флажок out, и нажмите кнопку Add.
  3. Нажмите кнопку 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. Введите в интерфейс следующие методы:

Найдите новые методы в IDL-файле и убедитесь, что мастер автоматически пронумеровал методы (1,2,...), присвоив им индексы типа DISPID:

[id(l), helpstring("method GetLightParams")]

HRESULT GetLightParams([out] int* pPos);

[id(2), helpstring("method SetLightParam")]

HRESULT SetLightParam([in] short Ip, [in] int nPos);

[id(3), helpstring("method ReadData")]

HRESULT ReadData(void);

[id(4), helpstring("method GetFillMode")]

HRESULT GetFillMode([out] DWORD* pMode);

[id(5), helpstring("method SetFillMode")]

HRESULT SetFillMode([in] DWORD nMode);

[id(6), helpstring("method GetQuad")]

HRESULT GetQuad([out] BOOL* bQuad);

[id(7), helpstring("method SetQuad")]

HRESULT SetQuad([in] BOOL bQuad);

С помощью этих индексов методы будут вызываться клиентами, получившими указатель на интерфейс диспетчеризации IDispatch. Мы уже обсуждали способ, который используется при вызове методов по индексам DISPID. Непосредственный вызов производит метод IDispatch: : invoke. Тот факт, что наш объект поддерживает IDispatch, мы определили при создании ATL-заготовки. Если вы не забыли, то мы тогда установили переключатель типа интерфейса в положение Dual. Это означает, что объект будет раскрывать свои методы как с помощью vtable, так и с помощью IDispatch. Некоторые детали этого процесса обсуждались в предыдущем уроке.

Ручная коррекция класса

Класс COpenGL будет обслуживать окно внедренного СОМ-объекта. Он должен иметь достаточное количество данных и методов для управления изображаемой поверхностью, поэтому далее вручную введем сразу много изменений в файл с описанием класса COpenGL. При изменении файла заголовков класса мы нарушим стиль, заданный стартовой заготовкой, и вернемся к более привычному, принятому в MFC-приложениях. Перенесем существующее тело конструктора, а также функции OnDraw в файл реализации класса OpenGLcpp. В файле OpenGLh останутся только декларации этих функций. Ниже приведено полное описание класса COpenGL с учетом нововведений, упрощений и исправлений. Вставьте его вместо того текста, который есть в файле OpenGLh. После этого вставим в файл новые сущности с помощью инструментов Studio.Net:

// OpenGL.h : Declaration of the COpenGL

#pragma once

#include "resource.h" // main symbols

#include <atlctl.h>

#include "_IOpenGLEvents_CP.h"

//========== Вспомогательный класс

class CPointSD

public:

fldat x;

float y;

float z; // Координаты точки в 3D

//====== Набор конструкторов и операция присвоения

CPoint3D () { х = у = z = 0; }

CPoint3D (float cl, float c2, float c3)

x = с1;

z = c2;

у = сЗ;

CPoint3D& operator=(const CPoint3D& pt)

x = pt.x;

z = pt. z ;

У = pt.y;

return *this;

}

CPointSD (const CPoint3D& pt) *this = pt;

//==== Основной класс, экспонирующий интерфейс IQpenGL

class ATL_NO_VTABLE COpenGL :

p.ublic CQomObjectRootEx<CComSingleThreadModel>,

public CStockPropImpKCOpenGL, IOpenGL>,

public IPersistStreamInitImpl<COpenGL>,

public I01eControlImpl<COpenGL>,

public I01eObjectImpl<COpenGL>,

public I01eInPlaceActiveObjectImpl<COpenGL>,

public IViewObjectExImpl<COpenGL>,

public I01eInPlaceObjectWindowlessImpl<COpenGL>,

public ISupportErrorlnfo,

public IConnectionPointContainerImpl<COpenGL>,

public CProxy_IOpenGLEvents<COpenGL>,

public IPersistStorageImpl<COpenGL>,

public ISpecifyPropertyPagesImpl<COpenGL>,

public IQuickActivateImpl<COpenGL>,

public IDataObjectImpl<COpenGL>,

public IProvideClassInfo2Impl<&CLSID_OpenGL,

&_uuidof(_IOpenGLEvents), &LIBID_ATLGLLib>,

public CComCoClass<COpenGL, &CLSID_OpenGL>,

public CComControl<COpenGL>

{

public:

//===== Переменные, необходимые

для

реализации интерфейса

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. Мы видим, что в заготовке присутствуют следующие биты:

Карты интерфейсов и свойств

Далее по коду вы видите карту макросов 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. По умолчанию предложен набор из двух неразлучных флагов:

Макрос 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. Для этого:

  1. Поставьте курсор на строку с именем класса COpenGL в окне ClassView и дайте команду Properties из контекстного меню.
  2. Нажмите кнопку Messages на панели инструментов окна Properties.
  3. Для того чтобы после введения обработчика окно свойств не убегало, переведите его в режим Floating и оттащите в сторону. В окне Class View должен быть выбран класс COpenGL
  4. По очереди для всех перечисленных сообщений укажите действие <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)

glEndO; }

//====== Закрываем блок команд GL_QUADS

if (m_bQuad) glEndO ;

//====== Закрываем список команд 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.

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

Просмотрите результаты. Прежде всего убедитесь, что в проекте появился новый класс CPropDlg, который поддерживает функциональность страницы свойств и окна диалога. Однако, запустив сервер и вызвав из контекстного меню его свойства, вы не увидите новой страницы. Там будут только те две страницы, которые были и до момента, как вы подключили поддержку страницы свойств. Для того чтобы новая страница действительно попала в блок страниц элемента, надо ввести новый элемент в карту свойств разрабатываемого элемента COpenGL. Откройте файл OpenGL.h и найдите в нем карту свойств. Она начинается строкой:

BEGIN_PROP_MAP(COpenGL)

Введите в нее новый элемент:

PROP_ENTRY("Свет", 1, CLSID_PropDlg)

который привязывает (binds) новую страницу к существующему блоку страниц свойств. Как видите, страница создается и связывается с объектом COpenGL по правилам СОМ, то есть с помощью уникального идентификатора ко-класса CLSlD_PropDlg. Единица определяет индекс DISPID (dispatch identifier) — 32-битный идентификатор, который используется упоминавшейся выше функцией invoke для идентификации методов, свойств и аргументов. Карта свойств теперь должна выглядеть следующим образом:

BEGIN_PROP_MAP(COpenGL)

PROP_DATA_ENTRY("_cx", m_sizeExtent.ex, VT_UI4)

PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VT_UI4)

PROP_ENTRY("FillColor", DISPID_FILLCOLOR, CLSID_StockColorPage)

PROP_ENTRY("CBeT", 1, CLSID_PropDlg) END_PROP_MAP()

Здесь важно уяснить, что каждая строка типа PROP_ENTRY соответствует какой-то функциональности, скрытой в каркасе сервера. Например, стандартное свойство Fill Color реализовано с помощью одной переменной m_clrFillColor и пары функций FillColor, упоминания о которых вы видели в IDL-файле. Тела этих функций остались за кулисами. То же справедливо относительно страницы свойств.

Важным моментом является появление нового ко-класса в составе библиотеки типов, генерируемой DLL-сервером. В коде, приведенном ниже, отметьте появление строк, связанных с ко-классом PropDlg и, конечно, не обращайте внимание на идентификаторы CLSID, которые могут не совпадать даже с предыдущей версией в этой книге, так как в процессе разработки сервера мне приходится неоднократно повторять заново процесс создания ко-классов:

Каждый раз при этом идентификаторы CLSID обновляются, и ваш реестр распухает еще больше. Хорошим правилом для запоминания в этом случае является следующее. Убирайте регистрацию всего сервера каждый раз, когда вы целиком убираете какой-либо неудачный ко-класс. Это, как мы отмечали, делается с помощью команды Start > Run > regsvr32 -u "C:\My Projects\ATLGL\ Debug\ATLGL.dll.". Перед тем как нажать кнопку ОК, внимательно проверьте правильность файлового пути к вашему серверу.

library ATLGLLib

{

importlib("stdole32.tlb");

importlib("stdole2.tlb") ;

[

uuid(6DEBB446-C43A-4AB5-BEEl-110510C7AC89)

helpstring("_IOpenGLEvents Interface")

]

dispinterface _IOpenGLEvents

{

properties:

methods:

};

[

uuid(5B3EF182-CD91-426F-9309-2E4869C353DB),

helpstringC'OpenGL Class")

]

coclass COpenGL

{

[default] interface IQpenGL;

[default, source] dispinterface _IOpenGLEvents;

};

//====== Новые элементы в библиотеке типов сервера

[

uuid(3AE16CD6-4558-460F-8A7E-5AB83D40DE9A),

helpstring("_IGraphPropEvents Interface")

]

dispinterface _IGraphPropEvents

{

properties:

methods:

};

[

uuid(lAOC756A-DA17-4630-91BO-72722950B8F7) ,

helpstring("GraphProp Class")

]

coclass PropDlg

{

interface lUnknown;

[default, source] dispinterface _IGraphPropEvents;

};

Убедитесь, что в составе проекта появились новые файлы (PropDlg. h, PropDlg. cpp и PropDlg. rgs). Откройте первый файл описаний и отметьте, что класс CPropDlg происходит от четырех родителей (классов ATL и одного интерфейса). Два из них (ccomObjectRootEx и CGomCoClass) мы уже встречали ранее, а два других (iPropertyPagelmpl и CDialoglmpl), как нетрудно догадаться, поддерживают функциональность диалоговой вкладки (страницы), размещаемой в блоке страниц (property sheet), и самого диалога, то есть механизм обмена данными. Оба родителя являются шаблонами, которые уже настроены на наш конкретный класс CPropDlg. Конструктор класса:

CPropDlg()

{

m_dwTitleID = IDSJTITLEPropDlg;

m_dwHelpFileID = IDS_HELPFILEPropDlg;

m_dwDocStringID = IDS_DOCSTRINGPropDlg;

}

устанавливает унаследованные переменные m_dwTitleio и идентификаторы строковых ресурсов в те значения, которые им присвоил мастер Studio.Net. Сами строки вы можете увидеть в ресурсах, если откроете узел дерева String Table. В классе изначально присутствует реакция на кнопку Apply, которая, как вы знаете, всегда сопровождает блок диалоговых вкладок (property sheet):

//====== Реакция на нажатие кнопки Apply

STDMETHOD(Apply)(void)

{

ATLTRACE(_T("CPropDlg::Apply\n"));

for (UINT i = 0; i < m_nObjects; i++)

{

// Do something interesting here

// ICircCtl* pCirc;

//m_ppUnk[i]->QueryInterface(IID_ICircCtl, (void**)SpCirc)

// pCirc->put_Caption(CComBSTR("smth special"));

// pCirc->Release();

}

m_bDirty = FALSE;

return S__OK;

}

В комментарий мастер поместил подсказку, которая дает намек о том, как следует пользоваться новым классом. Как вы видите, общение между двумя классами нашего сервера (copenGL и CPropDlg) должно происходить по правилам СОМ, то есть с помощью указателя на интерфейс. Этот факт производит впечатление излишней усложненности. Если оба класса расположены в рамках одной DLL, они могли бы общаться друг с другом с помощью прямого указателя, несмотря на то, что сама DLL загружается в пространство чужого процесса.

Имя ICircCtl, которое присутствует в подсказке, не имеет отношения к нашему проекту. Оно связано с учебным примером по созданию элементов управления с помощью библиотеки ATL. Вы можете увидеть этот пример в MSDN (Visual C++ Tutorials > Creating the Circle Control).

Переменная m_bDirty используется каркасом в качестве флага доступности кнопки Apply. Если m_bDirt у == FALSE; то кнопка недоступна. Она тотчас же должна стать доступной, если пользователь страницы диалога свойств введет изменения в органы управления на лице диалога. Конечно, этим состоянием управляет разработчик, то есть мы с вами.

Конструируем облик страницы свойств

Важным моментом в том, что произошло, когда вы добавили страницу свойств, является появление шаблона окна диалоговой вставки IDD_PROPDLG. Сейчас вам следует сконструировать облик этой вставки, разместив на ней элементы управления, необходимые для управления освещением. Кроме того, мы поместим туда кнопку вызова файлового диалога, выпадающий список для выбора одного из трех режимов заполнения полигонов и кнопку для переключения режима генерации поверхности (GL_QUADS или GL_QUAD_STRIP). Создайте с помощью редактора диалогов окно, примерный вид которого приведен на рис. 9.2. Вы, наверное, знаете, что нижний ряд кнопок поставляется блоком страниц (property sheet) и вам их вставлять не надо, необходимо сконструировать только облик самой страницы.

Рис. 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.

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

Редактор ресурсов может с возмущением отвергнуть ваш выбор. Тогда ищите другой диапазон с помощью уже рассмотренного диалога Resource Symbols. Эта тактика потенциально опасна. Повторюсь и скажу, что проще удалить и создать заново весь ресурс. Однако если вы самостоятельно выработаете или узнаете о более надежной технологии, то прошу сообщить мне. В этот момент следует запустить сервер и проверить наличие элементов на новой странице свойств. Если что-то не так, надо внимательно проверить, а возможно, и повторить все шаги создания вкладки.

Взаимодействие классов

Класс CPropDlg должен обеспечить реакцию на изменение регулировок, а класс COpenGL должен учесть новые установки и перерисовать изображение. Общение классов, как мы уже отметили, происходит по законам СОМ, то есть с помощью указателя на интерфейс. Здесь нам на помощь приходит шаблон классов CComQiPtr. Литеры «QI» в имени шаблона означают Querylnterface, что обещает нам автоматизацию в реализации запроса указателя на этот интерфейс. В классе переопределены операции выбора (->), взятия адреса (&), разадресации (*) и некоторые другие, которые упрощают использование указателей на различные интерфейсы. При создании объекта класса CComQiPtr, например:

CComQIPtr<IOpenGL, &IID_IOpenGL> р(m_ppUnk[i]) ;

он настраивается на нужный нам интерфейс, и далее мы работаем с удобствами, не думая о функциях Querylnterface, AddRef и Release. При выходе из области действия объекта р класса CGomQiPtr<lOpenGL, &ilD_iOpenGL> освобождение интерфейса произойдет автоматически.

Для обмена с окном диалоговой вставки введите в protected-секцию класса CPropDlg массив текущих позиций регуляторов и переменную для хранения текущего режима изображения полигонов:

protected:

int m_Pos[11]; BOOL m_bQuad;

В конструктор класса добавьте код инициализации массива:

ZeroMemory (m_Pos, sizeof(m_Pos));

Другую переменную следует инициализировать при открытии диалога (вставки). Способом, который вы уже неоднократно применяли, введите в класс реакции на Windows-сообщения WM_INITDIALOG и WM_HSCROLL. Затем перейдите к созданной мастером заготовке метода Onl nit Dialog, которую найдете в файле PropDlg.cpp:

LRESULT CPropDlg::OnInitDialog(UINT uMsg, WPARAM wParam,

LPARAM IParam, BOOL& bHandled)

{

_super::OnInitDialog(uMsg, wParam, IParam, bHandled);

return 1;

}

Здесь вы увидите новое ключевое слово языка _ super, которое является спецификой Microsoft-реализации. Оно представляет собой не что иное, как явный вызов родительской версии функции метода базового или super-класса. Так как классы в ATL имеют много родителей, то _ super обеспечивает выбор наиболее подходящего из них. Теперь введите изменения, которые позволят при открытии вкладки привести наши регуляторы в соответствие со значениями переменных в классе COpenGL. Вы помните, что значения регулировок используются именно там. Там же они и хранятся:

LRESULT CPropDlg: :OnInitDialog (UINT uMsg, WPARAM wParam,

LPARAM IParam, BOOL& bHandled)

_super::OnInitDialog(uMsg, wParam, IParam, -bHandled);

//====== Кроим умный указатель по шаблону IQpenGL

CComQIPtr<IOpenGL> p(m_ppUnk[0]);

//=== Пытаемся связаться с классом COpenGL и выяснить

//=== значение переменной m_FillMode

//=== В случае неудачи даем сообщение об ошибке

DWORD mode;

if FAILED (p->GetFillMode(&mode))

{

ShowError();

return 0;

}

//====== Работа с combobox по правилам API

//====== Получаем Windows-описатель окна

HWND hwnd = GetDlgItem(IDC_FILLMODE);

//====== Наполняем список строками текста

SendMessage(hwnd, CB_ADDSTRING, 0, (LPARAM)(LPCTSTR)"Points"

SendMessage(hwnd, CB_ADDSTRING, 0, (LPARAM)(LPCTSTR)"Lines")

SendMessage(hwnd, CB_ADDSTRING, 0, (LPARAM)(LPCTSTR)"Fill");

// Выбираем текущую позицию списка в соответствии

// со значением, полученным из COpenGL WPARAM

w = mode == GL_POINT ? 0

: mode == GL_LINE ?1:2;

SendMessage(hwnd, CB_SETCURSEL, w, 0);

// Повторяем сеанс связи, выясняя позиции ползунков

if FAILED (p->GetLightParams(m_Pos))

{

ShowError();

return 0;

}

// Мы не надеемся на упорядоченность идентификаторов

// элементов и поэтому заводим массив отображений

UINT IDs[] =

{

IDC_XPOS,

IDC_YPOS,

IDC_ZPOS,

IDC_AMBIENT,

IDC_DIFFUSE,

IDC_SPECULAR,

IDC_AMBMAT,

IDC_DIFFMAT,

IDC_SPECMAT,

IDC_SHINE,

IDC_EMISSION

};

//=== Пробег по всем регуляторам и их установка

for (int i=0;

Ksizeof (IDs)/sizeof (IDs [0] ) ; i++)

{

//====== Получаем описатель окна

hwnd = GetDlgItem(IDs[i]);

UINT nID;

//====== Узнаем идентификатор элемента

int num = GetSliderNum(hwnd, nID);

//====== Выставляем позицию

~ SendMessage(hwnd,TBM_SETPOS,TRUE,(LPARAM)m_Pos[i]

//=== Приводим в соответствие текстовый ярлык

char s [ 8 ] ;

sprintf (s,"%d",m_Pos[i]);

SetDlgltemText(nID, s);

}

// Выясняем состояние режима изображения полигонов

if FAILED (p->GetQuad(&m_bQuad))

{

ShowError ();

return 0;

}

//====== Устанавливаем текст

SetDlgltemText (IDC_QUADS,m_bQuad ? '"Quads" : "Strips");

return 1 ;

}

В процессе обработки сообщения нам понадобились вспомогательные функции GetSliderNum и ShowError. Первая функция уже участвовала в проекте на основе MFC, поэтому мы лишь напомним, что она позволяет по известному Windows-описателю окна элемента управления получить его порядковый номер в массиве позиций регуляторов. Кроме этого, функция позволяет получить идентификатор элемента управления nio, который нужен для управления им, например: при вызове SetDlgltemText (nID, s);.

int CPropDlg: : GetSliderNum (HWND hwnd, UINT& nID)

{

// Получаем ID по известному описателю окна

switch (: :GetDlgCtrlI)(hwnd) )

{

case IDC_XPOS:

nID = IDC_XPOS_TEXT;

return 0; case IDC_YPOS:

nID = IDC_YPOS_TEXT;

return 1 ; case IDC_ZPOS:

nID = IDC_ZPOS_TEXT;

return 2; case IDC_AMBIENT:

nID = IDC_AMB_TEXT;

return 3; case IDC_DIFFUSE:

nID = IDC_DIFFUSE_TEXT;

return 4 ;

case IDC_SPECULAR:

nID = 1DC_SPECULAR_TEXT;

return 5; case IDC_AMBMAT:

nID = IDC_AMBMAT_TEXT;

return 6; case IDC_DIFFMAT:

nID = IDC_DIFFMAT_TEXT;

return 7; case IDC_SPECMAT:

nID = IDC_SPECMAT_TEXT;

return 8; case IDC_SHINE:

nID = IDC_SHINE_TEXT;

return 9; case IDC_EMISSION:

nID = IDC_EMISSION_TEXT;

return 10;

}

return 0;

}

Функция showError демонстрирует, как в условиях СОМ можно обработать исключительную ситуацию. Если мы хотим выявить причину ошибки, спрятанную в HRESULT, то следует воспользоваться методом GetDescription интерфейса lErrorinfо. Сначала мы получаем указатель на него с помощью объекта класса ccomPtr. Этот класс, так же как и CGomQiPtr, автоматизирует работу с методами главного интерфейса lUnknown, за исключением метода Queryinterface:

void CPropDlg::ShowError()

{

USES_CONVERSION;

//====== Создаем инерфейсный указатель

CComPtr<IErrorInfo> pError;

//====== Класс для работы с Unicode-строками

CComBSTR sError;

//====== Выясняем причину отказа

GetErrorlnfo (0, &pError);

pError->GetDescription(SsError);

// Преобразуем тип строкового объекта для вывода в окно MessageBox(OLE2T(sError),_T("Error"),MB_ICONEXCLAMATION);

}

Если вы построите сервер в таком виде, то вас встретит неприятное сообщение о том, что ни один из явных или неявных родителей CPropDlg не имеет в своем составе функции OninitDialog. Обращаясь за справкой к документации (по классу CDialogimpl), мы убеждаемся, что это действительно так. Значит, инструмент Studio.Net, который создал заготовку функции обработки, не прав. Но как же будет вызвана наша функция OninitDialog, если она не является виртуальной функцией одного из базовых классов? Ответ на этот вопрос, как и на большинство других, можно получить в режиме отладки.

Закомментируйте строку вызова родительской версии, которая производится с помощью многообещающего ключевого слова _super (это и есть лекарство), поставьте точку останова на строке, следующей за ней, и нажмите F5. Если вы не допустили еще одной, весьма вероятной, ошибки, то тестовый контейнер сообщит, что он не помощник в процессе отладки, так как не содержит отладочной информации. Согласитесь с очевидным фактом, но не делайте поспешного вывода о том, что невозможно отлаживать все СОМ-серверы. В тот момент, когда вы инициируете новую страницу свойств, отладчик возьмет управление в свои руки и остановится на нужной строке программы. Теперь вызовите одно из самых полезных окон отладчика по имени Call stack, в нем вы увидите историю вызова функции OninitDialog, то есть цепочку вызовов функций. Для этого:

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

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

DECLARE_REGISTRY_RESOURCEID(IDR__PROPDLG)

то макрос окажется безоружным против такой атаки, хотя по идее он должен сопротивляться и даже не замечать наскоков подобного рода. Возможно, этот феномен исчезнет в окончательной версии Studio.Net.

Сообщение о прокрутке в окне

Сообщение 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;

}

В данный момент вы можете проверить функционирование регуляторов в суровых условиях СОМ. Они должны работать.

Реакция на выбор в окне выпадающего списка

Теперь введем реакцию на выбор пользователем новой строки в окне выпадающего списка. Для этого выполните следующие действия:

  1. Откройте в окне редактора Studio.Net шаблон окна диалога IDD_PROPDLG.
  2. Поставьте фокус в окно выпадающего списка IDC_FILLMODE и переведите фокус окно Properties.
  3. Нажмите кнопку Control Events, расположенную на инструментальной панели окна Properties.
  4. Найдите строку с идентификатором уведомляющего сообщения CBN_SELCHANGE и в ячейке справа выберите действие <Add>, для того чтобы там появилось имя функции обработки OnSelchangeFillmode.
  5. Перейдите в окно 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.

Создание контейнера на базе MFC

До сих пор для отладки и демонстрации нашего ActiveX-элемента мы пользовались услугами тестового контейнера ActiveX Control Test Container,который входит в состав инструментов Studio.Net. Пришла пора показать, как с помощью библиотеки классов MFC можно создать свой собственный простой контейнер, специализированный для управления элементом OpenGL Class.

  1. Создайте новый основанный на диалоге MFC-проект и назовите его TestGL.
  2. В окне вновь созданного шаблона диалога IDD_TESTGL_DIALOG вызовите контекстное меню и выберите команду Insert ActiveX Control.
  3. В окне появившегося диалога Insert ActiveX Control найдите строку OpenGL Class и нажмите ОК.
  4. Вы должны увидеть рамку нового элемента, размещенного Studio.Net в форме диалога. Элементу присвоен идентификатор IDCJDPENGLI, который можно увидеть в окне Properties. Уберите из него завершающую единицу.
  5. Растяните окно нового элемента так, чтобы оно занимало примерно 80% площади всего окна диалога (рис. 9.3).

Рис. 9.3. Внедрение элемента ActiveX в окно диалогового приложения

В отличие от Visual Studio б в конце этой процедуры в состав проекта (по умолчанию) не будет включен новый класс-оболочка (wrapper class) под именем CGpenGL. Такой класс необходим для дальнейшей работы с внедренным элементом ActiveX.

В документации бета-версии Studio.Net я нашел лишь намек на то, что wrapper-класс может быть создан с помощью ClassWizard. Однако мне не удалось добиться этого. Поэтому мы создадим класс-оболочку вручную. Конечно, здесь я использую заготовку класса, полученную в рамках Visual Studio 6. Она оказалась вполне работоспособной и в новой Studio.Net. Будем надеяться, что в следующих версиях Studio.Net рассмотренный процесс автоматического создания класса будет достаточно прозрачен.

Класс-оболочка

Обычно при создании приложения-контейнера для элемента ActiveX придерживаются следующей стратегии:

  1. Вставляют уже зарегистрированный элемент ActiveX в форму приложения контейнера, используя так называемую галерею объектов (Gallery).
  2. В одном из классов контейнера определяют переменную того же типа, что и класс-оболочка для внедренного элемента.
  3. Программируют поведение элемента, управляя им с помощью этой переменной.

Первый шаг этого алгоритма вы уже выполнили, теперь введите в состав проекта два новых файла 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& GetClsidO

{

// Следующая строка взята из файла 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, так как введение объекта влечет сразу несколько строк изменения кода.

  1. Поставьте фокус на окно внедренного элемента IDC_OPENGL в форме диалога и вызовите контекстное меню.
  2. В меню выберите команду Variable, которая запустит мастер Add Member Variable Wizard.
  3. Установите флажок Control Variable и задайте в полях диалоговой страницы мастера следующие значения: Access — public, Variable type — COpenGL, Variable name — * m_Ctrl, Control ID - IDC_OPENGL
  4. Обратите внимание на то, что в.поле Control type уже выбран тип элемента OCX, и нажмите кнопку Finish.

Результатом работы мастера являются следующие строки программы:

Для обеспечения видимости вставьте в начало файла 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.

Таблица 9.2. Идентификаторы элементов управления

Элемент

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

Диалог

IDD_TESTGL_DIALOG

Кнопка Data File

IDCJILENAME

Кнопка Back Color

IDC.BKCLR

Переключатель Quads

IDC_QUADS

Переключатель Strips

IDC_STRIPS

Выпадающий список Fill Mode

IDC_FILL

Ползунок Light (X)

IDC_XPOS

Кнопка Close

IDOK

Для кнопки Quads установите свойство Group в положение True, а для кнопки Strips — в False. Обе они должны иметь свойство Auto в состоянии True. Важно еще то, что числовые значения их идентификаторов должны следовать по порядку. Для кнопки Data File установите свойство DefaultButton. Для выпадающего списка снимите свойство Sort (сделайте его False) и слегка растяните вниз его окно в открытом состоянии, для этого сначала нажмите кнопку раскрывания. Для ползунка вы можете установить свойство Point в положение Top/Left. Обратите внимание на тот факт, что в режиме дизайна вы можете открыть с помощью правой кнопки мыши диалог со страницами свойств для элемента IDC_OPENGL, одну из которых мы создавали в предыдущем проекте. Теперь с помощью Studio.Net введите в диалоговый класс обработчики следующих событий:

Ниже мы приведем тела этих функций, а сейчас отметим, что все они пользуются услугами класса-оболочки для прямого вызова методов СОМ-сервера. Однако, как вы могли заключить из рассмотрения кодов класса 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. Сервер должен достаточно быстро реагировать на изменение регулировок органов управления клиентского приложения.

Подведем итог. В этом уроке мы научились:

Оглавление | Предыдущий Урок | Следующий Урок