Организация сайтостроительства Разработка стилей ActiveX Язык HTML Моделирование сайта Пакет Adobe GoLive WEB системы Протокол WAP 3D графика LightWave 8 3DS MAX 3D Studio MAX Мультимедийный сайт Анимация Графика Фото Кино Flash Видео на сайте Организация видео контента Premiere Pro Vstudio VirtualDub ВSonic Scenarist Видео на DVD Графика на сайте Изображения для сайта Photoshop Adobe After Effect Adobe Illustrator CorelDRAW CorelXARA Maya |
|








Dx=Vox+(Vex/Wex)*(Lx-Wox)
где: Dx — аппаратная (device) или физическая Х-координата точки,
Lx - логическая (logical) Х-координата точки,
Vex - протяженность области вывода, задаваемая SetVievvportExt,
Wex — протяженность окна, задаваемая SetWindowExt,
Vox — X начала координат области вывода (SetViewportOrg),
Wox — X начала координат логического окна (SetWindowOrg).
Аналогичная формула справедлива для Y-координаты точки. Опробуем формулу на произвольном наборе данных. (Такого типа вопросы вы можете встретить на сертификационном экзамене Microsoft.) Предположим, что в режиме MM_ANISOTROPIC заданы такие параметры отображения:
//====== Выделенная точка в логическом окне
pDC->SetWindowOrg (300, 0) ;
//====== Выделенная точка в физическом окне
pDC->SetViewportOrg (200, 200);
//====== Протяженность логического окна pDC->SetWindowExt (100, 100);
//====== Протяженность физического окна
pDC->SetViewportExt (50, -200);
Какие координаты в окне на экране будут иметь точки, заданные оператором: CPoint pl(0, 0), р2(100, 100)? Ответ: они преобразуются в аппаратные (физические) координаты: (50, 200) и (100, 0). Проверим первую координату подстановкой в формулу:
200 + 50/100(0-300)=50
Таким образом, если мы хотим увеличить или уменьшить изображение, то нужно изменять следующие величины:
Vex / Wex— коэффициент растяжения (сжатия) вдоль оси X,
Vey / Wey— коэффициент растяжения (сжатия) вдоль оси Y.
Эти формулы работают независимо только в режиме MM_ANISOTROPIC. Несколько иначе они работают в режиме MM_ISOTROPIC и вовсе не работают в остальных шести режимах.
В режиме MM_ISOTROPIC система обеспечивает одинаковое расширение (сжатие) по обеим осям, поэтому результат вычислений по приведенным формулам зависит от соотношения величин коэффициентов растяжения (сжатия). Теперь видно, что режим MM_ANISOTROPIC обеспечивает наибольшую свободу и гибкость в преобразовании координат. Числитель и знаменатель в формулах для коэффициентов растяжения (сжатия) задаются по отдельности с помощью методов класса CDC. Метод SetviewportExt задает числитель обоих отношений, следовательно, определяет свойства физического устройства, а метод SetwindowExt задает знаменатель, то есть задает свойства логической системы координат. Имена функций вводят нас в заблуждение, так как на самом деле эти функции ничего не задают и не определяют. Работая в паре, они дают способ задания двух вещественных коэффициентов растяжения (сжатия) путем задания четырех целых чисел. Параметры функций должны быть целыми или объектами класса csize, которые тоже создаются из двух целых. Значительно проще было бы задать два вещественных числа и использовать их в качестве коэффициентов. Объяснение наблюдаемой реальности, видимо, кроется в истории разработки функций API (сложности с floating point-арифметикой).




|
Вставка значка
Примечание 1 Примечание 1 Если вы найдете изображение одного глаза (скажем, левого) и откроете его в рамках студии, то изображение можно скопировать в новый ресурс типа Icon и перевернуть (сделать глаз правым), дав команду Image > Flip Horizontal. Исследуйте и другие команды этого меню. Элементы управления типа Picture Control можно сделать «чувствительными». Покажем, как ввести в приложение способность реагировать на нажатие кнопки мыши в области, занимаемой нашими изображениями глаз. По схеме, которую вы использовали, когда вводили в класс диалога реакцию на WM_HSCROLL, создайте функцию — обработчик сообщения WM_LBUTTONDOWN (нажата левая кнопка мыши). В тело заготовки для функции-обработчика внесите следующий код: void CLookDlg::OnLButtonDown(UINT nFlags, CPoint point) { CRect left, right; //====== Узнаем координаты левой картинки GetDlgItem{IDC_LEFT)->GetWindowRect(Sleft); //====== Переход к относительным координатам ScreenToClient(Sleft); //====== Узнаем координаты правой картинки GetDlgItem(IDC_RIGHT)->GetWindowRect(Sright) ; ScreenToClient(bright); //====== Объединяем площади двух картинок left.UnionRect(left,right); //====== Если координаты курсора внутри этой площади if (left.PtlnRect(point)) //======Вызываем диалог About OnSysCommand(IDM_ABOUTBOX,0); //====== Вызов родительской версии CDialog::OnLButtonDown(nFlags, point); } |

| 31-30 |
29 |
28 |
27-16 |
15-0 |
|
||
| Severity |
С |
R |
Facility |
Code |
|
| 31 |
30 |
29 |
28 |
27 |
15-0 |
26-16 |
| S |
R1 |
С1 |
N |
r |
Code |
Facility |
| Тип элемента |
Заголовок (комментарий) |
Идентификатор |
| Dialog |
WinError View |
IDD_LOOK_DIALOG |
| Group-box |
Error Number: |
IDC_STATIC |
| Spin |
|
IDC_SPIN |
| Edit |
// справа от IDC_SPIN |
IDC_CURRENT |
| Slider |
|
IDC_SLIDER |
| Text |
Total: |
IDC_STATIC |
| Text |
// под Total: |
IDCJTOTAL |
| Button |
Close |
IDCANCEL |
| Group-box |
Parameters: |
IDC_STATIC |
| Text |
Error Code: |
IDC_STATIC |
| Text |
// справа от Error Code: |
IDC_CODE |
| Text |
Find: |
IDC_STATIC |
| Edit |
// справа от Find: |
IDC_FIND |
| Picture |
|
IDC_RIGHT |
| Picture |
|
IDCJ.EFT |
| Text |
Severity: |
IDC_STATIC |
| Text |
// справа от Severity: |
IDC_SEVERITY |
| Text |
Facility: |
IDC_STATIC |
| Text |
// справа от Facility: |
IDC_FACILITY |
| Text |
Identifier: |
IDC_STATIC |
| Text |
// справа от Identifier: |
IDCJD |
| Text |
Message: |
IDC_STATIC |
| Text |
// справа от Message: |
IDC_MSG |
| Var |
m_Code |
m_ID |
m_Msg |
m_Severity |
m_FacHity |
| ID |
IDC_CODE |
IDCJD |
IDC_MSG |
IDC_SEVERITY |
IDC_FACILITY |
|
Вспомогательные функции |

| Элемент |
Идентификатор |
| Диалог |
IDD_POLYCOLOR |
| Окно редактирования Size |
IDC_PEN |
| Кнопка TRI |
IDCJTRI |
| Кнопка PENT |
IDC_ PENT |
| Кнопка STAR |
IOC_ STAR |
| Кнопка Close |
IDOK |
| Окно редактирования Red |
IDC_RED |
| Окно редактирования Green |
IDC_GREEN |
| Окно редактирования Blue |
IDC_BLUE |
| Ползунок (Slider) |
IDC_RSLIDER |
| Slider |
IDC_GSLIDER |
| Slider |
IDC_BSLIDER |
| Окно редактирования Color |
IDC_COLOR |
Нестандартные элементы управления
Рассмотрим, как создаются элементы управления, имеющие индивидуальный нестандартный облик. Сравнительно новым подходом в технологии создания таких элементов является обработка подходящего сообщения не в классе родительского окна, а в классе, связанном с элементом управления диалога. Такая возможность появилась в MFC начиная с версии 4.0, и она носит название Message Reflection. Элементы управления Windows посылают уведомляющие сообщения своим родительским (parent) окнам. Например, многие элементы, в том числе и Edit controls, посылают сообщение WM_CTLCOLOR, позволяющее родительскому окну выбрать кисть для закраски фона элемента. В версиях MFC (до 4.0), если какой-либо элемент должен выглядеть не так, как все, то эту его особенность обеспечивал класс родительского окна, обычно диалог. Теперь к старому механизму обработки уведомляющих сообщений от дочерних (child) элементов добавился новый, который позволяет произвести обработку уведомляющего сообщения в классе самого элемента. Уведомляющее сообщение как бы отражается (reflected) назад в класс дочернего окна элемента управления. Мы собираемся использовать нестандартные окна редактирования (Red, Green, Blue и Color), с тем чтобы они следили за изменением цвета, отражая текущий выбор как в числовом виде, так и в виде изменяющегося цвета фона своих окон. Эту задачу можно выполнить, создав класс (назовем его cclrEdit), производный от CEdit, и введя в него обработку отражаемого сообщения =WM CTLCOLOR.
Примечание 1
Примечание 1
Обратите внимание на символ = перед идентификатором сообщения Windows. Он необходим, чтобы различить два сообщения с одним именем. Наличие символа = означает принадлежность сообщения к группе отражаемых (reflected) сообщений.
Применяя уже известный вам подход, создайте класс cclrEdit с базовым классом CEdit. В процессе определения атрибутов нового класса укажите существующие файлы (PolyDlg.h и PolyDlg.cpp) в качестве места для размещения кодов нового класса. Если возникнут окна диалогов с просьбой подтвердить необходимость погружения кодов в уже существующие файлы, то ответьте утвердительно. Введите изменения в файл PolyDlg.h, так чтобы он приобрел следующий вид:
#pragma once
//===== Класс нестандартного окна редактирования
class CClrEdit : public CEdit
{
DECLARE_DYNAMIC (CClrEdit)
public:
CClrEdit () ;
virtual -CClrEdit () ;
void ChangeColor (COLORREF clr) ; // Изменяем цвета
protected:
DECLARE_MESSAGE_MAP ()
private :
COLORREF ra_clrText; // Цвет текста
COLORREF ra_clrBk; // Цвет фона
CBrush m_brBk; // Кисть для закраски фона
};
//====== Класс для управления немодальным диалогом
class CPolyDlg : public CDialog
{
friend class CClrEdit;
DECLARE_DYNAMIC (CPolyDlg)
public : enum ( IDD = IDD_POLYCOLOR } ;
//====== Удобный для нас конструктор
CPolyDlg (CTreeDoc* p) ;
virtual -CPolyDlg ( ) ;
//====== Отслеживание цвета
void UpdateColor () ;
protected: virtual void DoDataExchange (CDataExchange* pDX) ;
DECLARE_MESSAGE_MAP ( ) private :
CTreeDoc* m_pDoc; // Обратный указатель
CBitmapButton m_cTri; // Кнопки с изображениями
CBitmapButton m_cPent;
CBitmapButton m_cStar;
bool ra_bScroll; // Флаг использования ползунка };
};
Мы изменили конструктор класса CPolyDlg так, чтобы он имел один параметр — адрес документа, который мы используем в качестве обратного указателя. Это поможет нам управлять приложением, оставаясь в рамках методов диалогового класса. Теперь воспользуемся услугами Studio.Net для создания функции-обработчика сообщения =WM_CTLCOLOR в классе нестандартного окна редактирования.





| 1 | 0 | 0 | dx | ||
| C= | 0 | 1 | 0 | dy | |
| 0 | 0 | 1 | dz | ||
| 0 | 0 | 0 | 1 |
| 1 | 0 | 0|| | dx | || | x| | |x | +dx| | ||
| 0 | 1 | 0|| | dy | || | y| | = | |y | +dy| | |
| 0 | 0 | 1|| | dz | || | z| | |z | +dz| | ||
| 0 | 0 | 0|| | 1 | || | 1| | |1 |
Типы данных
OpenGL использует свои собственные типы данных, которые должны соответствовать аналогичным типам той платформы, на которой библиотека установлена. В Microsoft-реализации соответствие типов задано в файле заголовков GL.H так, как показано ниже. Эта таблица понадобится вам при анализе примеров и при разработке собственного кода:
typedef unsigned int GLenum;
typedef unsigned char GLboolean;
typedef unsigned int GLbitfield;
typedef signed char GLbyte;
typedef short GLshort;
typedef int GLint;
typedef int GLsizei;
typedef unsigned char GLubyte;
typedef unsigned short GLushort;
typedef unsigned int GLuint;
typedef float GLfloat;
typedef float GLclampf;
typedef double GLdouble;
typedef double GLclampd;
typedef void GLvoid;

Класс точки в 3D
С каждой вершиной, как вы помните, связано множество параметров, определяющих качество изображения OpenGL. Мы остановимся на наборе из трех величин: координаты вершины, вектор нормали и цвет. Так как вектор нормали и координаты можно задать с помощью двух объектов одного и того же типа (три вещественных переменных х, у, z), то целесообразно ввести в рассмотрение такое понятие, как точка трехмерного пространства. И воплотить его в виде класса CPoint3D, который инкапсулирует функциональность такой точки. Введите определение класса в конец файла Sphere. срр:
//====== Точка 3D-пространства
class CPointSD
{
public: float x, у, z; // Координаты точки
// ====== Конструктор по умолчанию
CPoint3D () { х = у = z = 0; ) //====== Конструктор с параметрами
CPointSD (double cl, double c2, float c3)
{
x = float (cl) ;
z = float(c2) ;
у = float(c3) ;
}
//====== Операция присвоения
CPoint3D& operator= (const CPoint3D& pt)
{
x = pt.x;
z = pt . z ;
У = Pt.y;
return *this;
//====== Операция сдвига в пространстве
CPoint3D& operator+= (const CPoint3D& pt)
{
x += pt.x;
y += Pt.y;
z += pt . z ;
return * this ;
}
//====== Конструктор копирования
CPointSD (const CPoint3D& pt)
{
*this = pt;
}
};
Обратите внимание на тот факт, что конструктор копирования использует код уже существующей операции присвоения. Имея в своем распоряжении класс CPointSD, мы можем создать еще один тип данных — структуру, поля которой объединяют все величины, связанные с вершиной треугольника. Массив данных такого типа будет хранить информацию обо всех вершинах изображения и при этом не будет повторений:
//====== Данные о вершине геометрического примитива
struct VERT
{
CPointSD v; // Координаты вершины
CPoiivt3D n; // Координаты нормали
DWORD с; // Цвет вершины
};
Введите эту декларацию после кода, определяющего CPoint3D. Как было отмечено, функция glDrawElements в качестве параметра требует задать массив индексов вершин. В соответствии с этими индексами вершины треугольников будут выбираться из общего массива вершин. Порядок следования индексов зависит от порядка обхода вершин при задании треугольников. Как вы помните, он должен идти против часовой стрелки, если смотреть на примитив с конца внешней нормали. В этом случае знак нормали соответствует формулам векторной алгебры,!: которые мы уже рассматривали.
Будет удобно, если мы сначала создадим структуру, которая объединяет три индекса вершин одного треугольника. Тогда массив структур такого типа сможет играть роль массива индексов, требуемого функцией glDrawElements. Введите следующее описание в продолжение файла:
struct TRIA
{
//====== Индексы трех вершин треугольника,
//====== выбираемых из массива вершин типа VERT
//====== Порядок обхода — против часовой стрелки
int i1;
int i2;
int i3;
};
Далее нам понадобятся две глобальные неременные типа CPointSD, с помощью *":' которых мы будем производить анимацию изображения сферы. Анимация, а также различие цветов при задании вершин треугольников позволят более четко передать трехмерный характер изображения. Наличие освещения подвижного объекта также заметно увеличивает его реалистичность. При создании програм-| мы мы обойдемся одним файлом, поэтому новые объявления продолжайте вставлять в конец файла Sphere.срр:
//====== Вектор углов вращения вокруг трех осей ?
CPointSD gSpin; //====== Вектор случайной девиации вектора gSpin
CPointSD gShift;
При каждой смене буферов (перерисовке изображения) мы будем вращать изоб- ; ражение сферы вокруг всех трех осей на некоторый векторный квант gshif t. Для того чтобы вращение было менее однообразным, введем элемент случайности. Функция Rand, приведенная ниже, возвращает псевдослучайное число в диапазоне (-х, х). Мы будем пользоваться этим числом при вычислении компонентов вектора gshif t. Последний, воздействуя на другой вектор gSpin, определяет новые значения трех углов вращения, которые функция glRotate использует для задания очередной позиции сферы:
inline double Rand(double x)
{
//====== Случайное число в диапазоне (-х, х)
return х - (х + х) * rand() / RAND_MAX;
}
Учитывая сказанное, можно создать алгоритм перерисовки:
void _stdcall OnDraw()
{
glClear(GL_COLOR_BUFFER_BIT) ;
//=== Сейчас текущей является матрица моделирования
glLoadldentityО;
//====== Учет вращения
glRotated(gSpin.х, 1., О, 0.) ;
glRotated(gSpin.y, 0., 1., 0.);
glRotated(gSpin.z, 0., 0., 1.) ;
//====== Вызов списка рисующих команд
glCallList(1);
//====== Подготовка следующей позиции сферы
gSpin += gShift;
//===== Смена буферов auxSwapBuffers();
}
Подготовка сцены
Изображение сферы целесообразно создать заранее (в функции init), а затем воздействовать на него матрицей моделирования, коэффициенты которой изменяются в соответствии с алгоритмом случайных девиаций вектора вращения. При разработке кода функции init надо учесть специфику работы с функцией glDrawElements, которая обсуждалась выше. Кроме того, здесь мы производим установку освещенности, технологию и детали которой можно выяснить в сопровождающей документации (MSDN). Введите следующие коды функции инициализации и вставьте их до функции перерисовки:
void Init ()
{
//=== Цвет фона (на сей раз традиционно черный)
glClearColor (0., 0., 0., 0.);
//====== Включаемаем необходимость учета света
glEnable(GL_LIGHTING);
//=== Включаемаем первый и единственный источник света
glEnable(GL_LIGHT());
//====== Включаем учет цвета материала объекта
glEnable(GL_COLOR_MATERIAL);
// Вектор для задания различных параметров освещенности
float v[4] =
{
0.0Sf, 0.0Sf, 0.0Sf, l.f
};
//=== Сначала задаем величину окружающей освещенности glLightModelfv(GL_LIGHT_MODEL_AMBIENT, v);
//====== Изменяем вектор
v[0] = 0.9f; v[l] = 0.9f; v[2] = 0.9f;
//====== Задаем величину диффузной освещенности
glLightfv(GL_LIGHTO, GL_DIFFUSE, v) ;
//======= Изменяем вектор
v[0] = 0.6f; v[l] = 0.6f; v[2] = 0.6f;
//====== Задаем отражающие свойства материчала
glMaterialfv(GL_FRONT, GL_SPECULAR, v);
//====== Задаем степень блесткости материала
glMateriali(GL_FRONT, GL_SHININESS, 40);
//====== Изменяем вектор
v[0] = O.f; v[l] = O.f; v[2] = l.f; v[3] = O.f;
//====== Задаем позицию источника света
glLightfv(GL_LIGHTO, GL_POSITION, v);
//====== Переключаемся на матрицу проекции
glMatrixMode(GL_PROJECTION); glLoadldentity();
//====== Задаем тип проекции
gluPerspective(45, 1, .01, 15);
//=== Сдвигаем точку наблюдения, отодвигаясь от
//=== центра сцены в направлении оси z на 8 единиц
gluLookAt (0, 0, 8, 0, 0, 0, 0, 1, 0) ;
//====== Переключаемся на матрицу моделирования
glMatrixMode(GL_MODELVIEW);
//===== Включаем механизм учета ориентации полигонов
glEnable(GL_CULL_FACE);
//===== Не учитываем обратные поверхности полигонов
glCullFace(GL_BACK);
//====== Настройка OpenGL на использование массивов
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
//====== Захват памяти под динамические массивы
VERT *Vert = new VERT[gnVert];
TRIA *Tria = new TRIA[gnTria];
//====== Создание изображения
Sphere(Vert, Trial;
//====== Задание адресов трех массивов (вершин,
//====== нормалей и цветов),
/1====== а также шага перемещения по ним
glVertexPointer(3, GL_FLOAT, sizeof(VERT), &Vert->v); glNormalPointer(GL_FLOAT, sizeof(VERT), &Vert->n);
glColorPointer(3, GL_UNSIGNED_BYTE, sizeof(VERT),
SVert->c);
srand(time(0)); // Подготовка ГСЧ
gShift = CPoint3D (Rand(gMax),Rand(gMax),Rand(gMax));
//====== Формирование списка рисующих команд
glNewListd, GL_COMPILE);
glDrawElements(GL_TRIANGLES, gnTria*3, GL_UNSIGNED_INT, Tria);
glEndList() ;
//== Освобождение памяти, так как список сформирован
delete [] Vert;
delete [] Tria;
}
Формула учета освещенности
Семейство функций glLightModel* позволяет установить общие параметры освещенности сцены. В частности, первый параметр GL_LIGHT_MODEL_AMBIENT сообщает OpenGL, что второй параметр содержит четыре компонента, задающие RGBA-интенсивность освещенности всей сцены. По умолчанию вектор освещенности сцены равен (0.2, 0.2, 0.2, 1.0). Команда glLight* устанавливает параметры источника света. Мы пользуемся ею два раза для задания диффузного и рефлективного компонента интенсивности света. Если вы обратитесь к документации, то увидите, что с помощью glLight* можно задать еще более десятка параметров источника света. Формулу учета освещения я нашел в документации лишь в словесном описании, но рискну привести ее в виде математического выражения.
В режиме RGBA-интенсивность каждого из трех компонентов цвета освещенной вершины вычисляется как сумма нескольких составляющих. Первая составляющая учитывает эмиссию света материалом, вторая — освещенность окружения (ambient) или всей сцены, третья — является суммой вкладов от всех источников света. Максимально допустимое число источников, как вы помните, определено константой GL_MAX_LIGHTS, которая в нашем случае равна 8:
L=Me+MaLaf+Сумма(MaLai+MdLdi(N*Vl)+MsLsi(Ve*Vl)^h)
Здесь символ т обозначает некоторое свойство материала, а символ / — свойство света. Индекс е в применении к материалу обозначает эмиссию, а в применении к
вектору v — eye (глаз). Остальные индексы в применении к материалу обозначают различные компоненты его отражающих свойств.
Ориентация поверхности
Кроме установки параметров света код функции init содержит довольно много других установок, которые мы осуществляем впервые, поэтому обсудим их более подробно. Возможно, вы помните из курса аналитической геометрии, что некоторые поверхности имеет ориентацию. По умолчанию поверхность любого полигона считается лицевой (FRONT), если вы задали ее обходом вершин против часовой стрелки, и она считается изнаночной (BACK), если направление обхода было обратным. В частности, ориентация поверхности влияет на ориентацию нормали.
Примечание 1
Примечание 1
Вы можете реверсировать эту установку, задав режим glfrontFace (GL_CW). По умолчанию действует установка glFrontFace(GL_CCW). Аббревиатура CW означает clockwise (по часовой стрелке), a CCW — counterclockwise (против часовой стрелки). Кстати, вы, вероятно, видели в литературе изображение ленты Мебиуса или бутылки Клейна, поверхности которых односторонние и поэтому не имеют ориентации.
Команда glEnable (GL_CULL_FACE); включает механизм учета ориентации поверхности полигонов. Она должна сопровождаться одним из флагов, определяющих сторону поверхности, например glCullFace(GL_BACK);. Таким образом, мы сообщаем конвейеру OpenGL, что обратные стороны полигонов можно не учитывать. В этом случае рисование полигонов ускоряется. Мы не собираемся показывать внутреннюю поверхность замкнутой сферы, поэтому эти установки нам вполне подходят.
Массив вершин, нормалей и цветов
Три команды glEnableClientstate говорят о том, что при формировании изображения будут заданы три массива (вершин, нормалей и цветов), а три команды вида gl* Pointer непосредственно задают адреса этих массивов. Здесь важно правильно задать не только адреса трех массивов, но и шаги перемещения по ним. Так как мы вместо трех массивов пользуемся одним массивом структур из трех полей, то шаг перемещения по всем трем компонентам одинаков и равен sizeof (VERT) — размеру одной переменной типа VERT. Массив вершин (vert типа VERT*) и индексов их обхода (Tria типа TRIA*) создается динамически внутри функции init. Характерно, что после того, как закончилось формирование списка рисующих команд OpenGL, мы можем освободить память, занимаемую массивами, так как вся необходимая информация уже хранится в списке. Формирование массивов производится в функции Sphere, которую еще предстоит разработать.
Далее по коду Init идет формирование списка рисующих команд. Так как массивы вершин и индексов их обхода при задании треугольников уже сформированы, то список рисующих команд создается с помощью одной команды glDrawElements. Ее параметры указывают:
Формирование массива вершин и индексов
Самой сложной задачей является правильное вычисление координат всех вершин треугольников и формирование массива индексов Tria, с помощью которого команда glDrawElements обходит массив Vert при задании треугольников. Функция Sphere реализует алгоритм последовательного обхода сначала всех сферических треугольников вокруг полюсов сферы, а затем обхода сферических четырехугольников, образованных пересечением параллелей и меридианов. В процессе обхода формируется массив вершин vert. После этого обходы повторяются для того, чтобы заполнить массив индексов Tria. Северный и южный полюса обрабатываются индивидуально. Для осуществления обхода предварительно создаются константы:

Списки команд OpenGL (Display Lists)
Все данные, описывающие геометрию или отдельные пикселы, могут быть сохранены в списках команд (display lists) для последующего использования. Альтернатива — немедленное использование (immediate mode). При вызове списка командой glCallList сохраненные данные из списка начинают двигаться по конвейеру так же, как и в режиме немедленного использования.
Вычислители (Evaluators)
Все геометрические примитивы описываются своими вершинами. Параметрические кривые и поверхности могут изначально-быть описаны контрольными точками или базовыми функциями (обычно полиномиальными). Вычислители — это методы, которые генерируют координаты вершин, нормали к поверхности, координаты текстур и цвета точек, опираясь на контрольные точки.
Сборка примитивов
На этом этапе происходит преобразование вершин в примитивы. Пространственные координаты (х, у, z) преобразовываются с помощью матриц размерностью (4 х 4). Основная цель — получить экранные, двухмерные координаты из трехмерных, мировых координат. Если включен режим генерации текстуры, то она создается на этом этапе. Освещенность вычисляется исходя из координат вектора нормали, расположения источников света, отражающих свойств материала, углов конусов света и параметров его аттенюации (ослабления). В результате получается цвет пиксела. Важным моментом на этапе сборки примитивов (primitive assembly) является отсечение (clipping), то есть удаление тех частей геометрии, которые попадают в невидимую часть пространства. Точечное отсечение пропускает или не пропускает вершину. Отсечение линий или полигонов подразумевает не только удаление вершин, но и возможное добавление некоторых (промежуточных) вершин. На этом этапе происходит учет перспективы, то есть уменьшение тех деталей сцены, которые расположены дальше от точки наблюдения, и увеличение тех деталей, которые расположены ближе. Здесь используется понятие видимого объема (viewport). Режим заполнения промежуточных точек полигона тоже играет роль на этапе сборки.
Операции с пикселами (Pixel Operations)
Данные о пикселах следуют в конвейере OpenGL параллельным путем. Данные, хранимые в массивах системной памяти, распаковываются с учетом набора возможных форматов, затем масштабируются, сдвигаются и обрабатываются так называемой картой пикселов (pixel map). Результат записывается либо в память текстуры, либо посылается на следующий этап — растеризацию. Отметьте, что возможна обратная операция считывания пикселов. При этом также Выполняются операции: масштабирование, сдвиг, преобразование и упаковка и помещение в системную память. Существуют специальные операции копирования данных из буфера кадра (framebuffer) в другую его часть или в буфер текстуры.
Сборка текстуры (Texture Assembly)
Текстуры — это bitmap-изображения, накладываемые на поверхности геометрических объектов для придания эффекта фактуры реального материала. Текстурные объекты создаются в OpenGL для упрощения их повторного использования. Использование текстур сопряжено с большими затратами, поэтому в работе с ними применяют специальные ресурсы, такие как texture memory. Так называют быструю видеопамять, приоритет использования которой отдается текстурным объектам.
Растеризация
Так называют преобразование как геометрических, так и данных о пикселах во фрагменты. Каждый фрагмент соответствует пикселу в буфере кадра. При вычислении цвета фрагмента учитывается большое количество факторов: узор штриховки полигона или линии, толщина линии и размер точки, сглаживание зубчатости линий, тень объекта, режим заполнения полигона, учет глубины изображения (факт видимости или невидимости) и др.
Операции с фрагментами
Каждая точка уже двухмерного изображения характеризуется цветом, глубиной (значением координаты Z) и данными о текстуре. Такая точка вместе с сопутствующей информацией называется фрагментом. Фрагмент изменяет соответствующий ему пиксел в буфере кадра, если он проходит пять тестов:

Если координаты векторов а и b известны, то координаты нормали вычисляю по следующим формулам. Длина вектора нормали п зависит от длин вектор сомножителей и величины угла между ними:
Nx=AxBz-AzBy
Ny=AzBx-AxBz
Nz=AxBy-AyBx
Примечание 1
Примечание 1
Можно потерять много времени на осознание того факта, что не только правление нормали, но и ее модуль влияют на величину освещенности (и та) вершины, так как сопровождающая документация (Help) не содер; явных указаний на это. Отметьте также, что цвета вершин полигона влияю цвета точек заполнения полигона, так как цвета вновь генерируемых то интерполируются, то есть принимают промежуточные значения между з чениями цвета вершин.
Чтобы нивелировать зависимость цвета вершины от амплитуды нормали, обыч вектор нормали масштабируют (или нормируют), то есть делают его длину р; ной единице, оставляя неизменным направление. С учетом сказанного создал две вспомогательные функции. Первая масштабирует, а вторая вычисляет н< маль к плоскости треугольника. Алгоритм вычисления использует координа двух сторон, прилегающих к текущей вершине треугольника:
//====Нормирование вектора нормали (или любого другого)
void Scale(double v[3])
{
double d = sqrt(v[0]*v[0]+v[l]*v[l]+v[2]*v[2]);
if (d == 0.)
{
MessageBox(0,"Zero length vector","Error",MB_OK);
return;
}
void getNorm(double vl[3], double v2[3], double out[3])
{
//===== Вычисляем координаты вектора нормали
//====== по формулам векторного произведения
out[0] = vl[l]*v2[2] - vl[2]*v2[l];
out[l] = vl[2]*v2(0] - vl[0]*v2[2] ;
out[2] =vl[0]*v2[l] - vl[l]*v2[0];
Scale(out);
}
Замените функцию DrawScene. В новом варианте мы аккуратно вычисляем и масштабируем нормали в каждом из двадцати треугольников поверхности икосаэдра:
void DrawScene()
{
static double
angle - 3. * atanfl.)/2.5, V = cos(angle), W = sin(angle),
v[12] [3] = {
{-V,0.,W}, {V,0.,W}, {-V,0.,-W},
{V,0.,-W}, {0.,W,V}, {0.,W,-V},
{0.,-W,V}, {0. ,-W,-V}, {W,V, 0.},
{-W,V,0.}, {W,-V,0.}, {-W,-V,0.}
};
static GLuint id[20][3] = {
(0,1, 4), {0,4, 9}, (9,4, 5), (4,8, 5}, (4,1,8),
(8,1,10), (8,10,3), (5,8, 3), (5,3, 2), (2,3,7),
(7,3,10), (7,10,6), (7,6,11), (11,6,0), (0,6,1),
(6,10,1), (9,11,0), (9,2,11), (9,5, 2), (7,11,2) 1;
glNewList(l,GL_COMPILE); glColorSd (1., 0.4, 1.) ;
glBegin(GLJTRIANGLES);
for (int i = 0; i < 20; i++)
{
double dl[3], d2[3], norm[3];
for (int j = 0; j < 3; j++)
{
dl[j] =v[id[i][0]] [j] -v[id[i][l]J [j];
d2[j] =v[id[i][l]] [j] -v[id[i][2J] [j];
}
//====== Вычисление и масштабирование нормали
getNorm(dl, d2, norm);
glNormal3dv(norm);
glVertexSdv(v [ id[i] [1]]);
glVertex3dv(v[id[i] [1] ] glVertex3dv(v[id[i] [2] ]
glEnd() ;
}
glEndList () ;
}
Примечание 2
Примечание 2
Функцию нормировки всех нормалей можно возложить на автомат OpenGL, если включить состояние GL_NORMALIZE, но обычно это ведет к замедлению перерисовки и, как следствие, выполнения приложения, если изображение достаточно сложное. В нашем случае оно просто, и поэтому вы можете проверить действие настройки, если вставите вызов glEnable (GL_NORMALIZE); в функцию Init (до вызова OrawScene) и временно выключите вызов Scale(out); производимый в функции getNorm. Затем вернитесь к исходному состоянию.

| 0x00, |
0x20, |
0x04, |
0x00, |
0x00, |
0x30 |
, 0x0c, |
0x00, |
||
| 0x00, |
0x10, |
0x08, |
0x00, |
0x00, |
0x18 |
, 0x18, |
0x00, |
||
| 0x07, |
0хс4, |
0x23, |
0xe0, |
0x0f, |
0xf8 |
, 0xlf, |
0xf0, |
||
| 0x38, |
0xlc, |
0x38, |
0xlc, |
0x30, |
0x00 |
, 0x00, |
0x0c, |
||
| 0x60, |
0x00, |
0x00, |
0x06, |
0x60, |
0x00 |
, 0x00, |
0x06, |
||
| 0x60, |
0x00, |
0x00, |
0x06, |
0x60, |
0x00 |
, 0x00, |
0x06, |
||
| 0x60, |
0x00, |
0x00, |
0x06, |
0x30, |
0x00 |
, 0x00, |
0x0c, |
||
| 0x30, |
0x00, |
0x00, |
0x0c, |
0x18, |
0x00 |
, 0x00, |
0x18, |
||
| 0х0е, |
0x00, |
0x00, |
0x70, |
0x03, |
0x00 |
, 0x00, |
0xc0, |
||
| 0x00, |
OxcO, |
0x03, |
0x00, |
0x00, |
0x70 |
, 0x0e, |
0x00, |
||
| 0x00, |
0x18, |
0x18, |
0x00, |
0x00, |
0x0c |
, 0x30, |
0x00, |
||
| 0x00, |
0x07, |
OxeO, |
0x00, |
0x00, |
0x03 |
, 0xc0, |
0x00, |
||
| 0x00, |
0x01, |
0x80, |
0x00, |
0x00, |
0x00 |
, 0x00, |
0x00 |
||
| GLubyte |
gStripU = |
// Другой узор - |
полоса |
|
|||||
| 0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
| 0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
| 0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
| 0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
| 0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
| 0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
| 0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
| 0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
| 0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
| 0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
| 0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
| 0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
| 0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
| 0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
| 0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
| 0x66, |
0x66, |
0x66, |
0x66, |
0x33, |
0x33 |
, 0x33, |
0x33, |
||
Xw=(X+1)(width/2)+X0
Yw=(Y+1)(height/2)+Y0
В левой части равенств стоят оконные координаты:
| (0,1, 4), (8,1,10), (7,3,10), (6,10,1), |
(0,4, 9), (8,10,3), (7,10,6), (9,11,0), |
(9,4, 5), (5,8, 3), (7,6,11), (9,2,11), |
(4,8, 5), (5,3, 2), (11,6,0), (9,5, 2), |
(4,1,8), (2,3,7), (0,6,1), (7,11,2) |
||
Точное вычисление нормалей
Проверьте результат и обсудите качество. В данном варианте нормали в вершинах заданы так, как будто изображаемой фигурой является сфера, а не икосаэдр. Это достаточно грубое приближение. Если поверхность произвольного вида составлена из треугольников, то вектор нормали к поверхности каждого из них можно вычислить точно, опираясь на данные о координатах вершин треугольника. Из $ курса векторной алгебры вы, вероятно, помните, что векторное произведение двух векторов а и b определяется как вектор п, перпендикулярный к плоскости, в которой лежат исходные векторы. Величина его равна площади параллелограмма, построенного на векторах а и b как на сторонах, а направление определяется так, что векторы a, b и п образуют правую тройку. Последнее означает, что если представить наблюдателя на конце вектора п, то он видит поворот вектора а к вектору b, совершаемый по кратчайшему пути против часовой стрелки. На Рисунок 6.4. изображена нормаль п (правая тройка) при различной ориентации перемножаемых векторов а и b.
Диалоговый класс Инициализация диалога Работа с группой регуляторов Особенности немодального режима Панель управления Стандартные свойства Тестирование объекта Разработка сервера Регистрация библиотеки типов Yi,j=exp[-(i+20*j)/256]*SIN[3*п* (i-Nz/2)/Nz]*SIN[3*п*(j-Nx/2)/Nx] Функция перерисовки Обзор класса COpenGL Карты интерфейсов и свойств Карта точек соединения Карта сообщений Интерфейс ISupportsErrorlnfо Интерфейс IViewObjectEx Карта объектов Реализация методов интерфейса Сообщение о прокрутке в окне Реакция на выбор в окне выпадающего списка Реакция на нажатия кнопок Управление объектом с помощью мыши Контейнеры типа priority_queue Пары Генерирование последовательности Шаблон функции вывода содержимого контейнера Вектор объектов класса Предикаты и функциональные объекты Параметры шаблона Формирование матрицы
Yi,j=[3*п*(i-Nz/2)/2*Nz]*SIN[3*п*(j-Nx/2)/2*Nx]
Здесь п. обозначает количество ячеек сетки вдоль оси Z, а пх — количество ячеек вдоль оси X. Индексы i (0 < i < пz) и j (0 < j < nx) выполняют роль дискретных значений координат (Z, X) и обозначают местоположение текущей ячейки при пробеге по всем ячейкам сетки в порядке, описанном выше. Остальные константы подобраны экспериментально так, чтобы видеть полтора периода изменения гармонической функции.
Мы собираемся работать с двоичным файлом и хранить в нем информацию в своем формате. Формат опишем словесно: сначала следуют два целых числа m_xsize и m_zSize (размеры сетки), затем последовательность значений функции у = f (х, z) в том же порядке, в котором они были созданы. Перед тем как записать данные в файл, мы поместим их в буфер, то есть временный массив buff, каждый элемент которого имеет тип BYTE, то есть unsigned char. В буфер попадают значения переменных разных типов, что немного усложняет кодирование, но зато упрощает процесс записи и чтения, который может быть выполнен одной командой, так как мы пишем и читаем сразу весь буфер. В процессе размещения данных в буфер используются указатели разных типов, а также преобразование их типов:
void COGView::DefaultGraphic()
{
//====== Размеры сетки узлов
m xSize = m zSize = 33;
//====Число ячеек на единицу меньше числа узлов
UINTnz = m_zSize - 1, nx = m_xSize - 1;
// Размер файла в байтах для хранения значений функции
DWORD nSize = m_xSize * m_zSize * sizeof (float) + 2*sizeof (UINT) ;
//====== Временный буфер для хранения данных
BYTE *buff = new BYTE[nSize+l] ;
//====== Показываем на него указателем целого типа
UINT *p = (UINT*)buff;
//====== Размещаем данные целого типа
*р++ = m_xSize;
*р++ = m_zSize;
//====== Меняем тип указателя, так как дальше
//====== собираемся записывать вещественные числа
float *pf = (float*)?;
//=== Предварительно вычисляем коэффициенты уравнения
double fi = atan(l.)*6,
kx = fi/nx,
kz = fi/nz;
//====== В двойном цикле пробега по сетке узлов
//=== вычисляем и помещаем в буфер данные типа float
for (UINT i=0; i
for (UINT j=0; j
{
*pf++ = float (sin(kz* (i-nz/2.) ) * sin (kx* (j-nx/2. ) )
}
}
//=== Переменная для того, чтобы узнать сколько
//=== байт было реально записано в файл DWORD nBytes;
//=== Создание и открытие файла данных sin.dat
HANDLE hFile = CreateFile (_T ("sin .dat") , GENERIC_WRITE,
0, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0)
//====== Запись в файл всего буфера
WriteFile (hFile, (LPCVOID) buff, nSize, SnBytes, 0) ;
//====== Закрываем файл
CloseHandle (hFile) ;
//====== Создание динамического массива m_cPoints
SetGraphPoints (buff, nSize) ;
//====== Освобождаем временный буфер
delete [] buff;
}
В процессе создания, открытия и записи в файл мы пользуемся API-функциями CreateFile, WriteFile и CloseHandle, которые предоставляют значительно больше возможностей управлять файловых хозяйством, чем, например, методы класса CFile или функции из библиотек stdio.h или iostream.h. Обратитесь к документации, для того чтобы получить представление о них.
Настройка проекта
Настройка проекта
Этот тип стартовой заготовки позволяет работать с окном (cocview), которое помещено в клиентскую область окна-рамки (CMainFrame), и создать в этом окне контекст передачи OpenGL. Класс документа нам не понадобится, так как мы собираемся производить все файловые операции самостоятельно, используя свой собственный двоичный формат данных. В связи с этим нам не нужна помощь в сериализации данных, которую предоставляет документ. Для использования функций библиотеки OpenGL надо сообщить компоновщику, чтобы он подключил необходимые библиотеки OpenGL, на сей раз только две.
Чтобы покончить с настройками общего характера, вставьте в конец файла StdAfx.h строки, которые обеспечивают видимость библиотеки OpenGL, а также некоторых ресурсов библиотеки STL:
#include
#include
//=== Подключение заголовков библиотек OpenGL
#include
#include
#include
using namespace std;
Параметры освещения
Параметры освещения
Установка параметрпв освещения осуществляется подобно тому, как это делалось в предыдущем уроке. Но здесь мы храним все параметры для тога, чтобы можно было управлять освещенностью изображения. Немного позже разработаем диалог, с помощью которого пользователь программы сможет изменять настройки освещения, а сейчас введите коды функции SetLight:
void COGView::SetLight()
{
//====== Обе поверхности изображения участвуют
//====== при вычислении цвета пикселов
//====== при учете параметров освещения
glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, 1);
//====== Позиция источника освещения
//====== зависит от размеров объекта
float fPos[] =
{
(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);
/1 ====== Интенсивность окружающего освещения
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]/100.f;
float fDiffuse[4] = { f, f, f, O.f };
glLightfv(GL_LIGHTO, GL_DIFFUSE, fDiffuse);
//====== Интенсивность отраженного света
f = m_LightParam[5]/100.f;
float fSpecular[4] = { f, f, f, 0.f };
glLightfv(GL_LIGHTO, GL_SPECULAR, fSpecular);
//====== Отражающие свойства материала
//====== для разных компонентов света
f = m_LightParam[6]/100.f;
float fAmbMat[4] = { f, f, f, 0.f };
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, fAmbMat);
f = m_LightParam[7]/100.f;
float fDifMat[4] = { f, f, f, 1.f };
glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, fDifMat);
f = m_LightParam[8]/100.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]/100.f;
float f Emission [4] = { f , f , f , 0 . f } ;
glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, fEmission) ;
}
Подготовка изображения
Подготовка изображения
Разработаем код функции DrawScene, которая готовит и запоминает изображение на основе координат вершин, хранимых в контейнере m_cPoints. Изображение по выбору пользователя формируется либо в виде криволинейных четырехугольников (GL_QUADS), либо в виде полосы связанных четырехугольников (GL_QUAD_STRIP). Точки изображаемой поверхности расположены над регулярной координатной сеткой узлов в плоскости (X, Z). Размерность этой сетки хранится в переменных m_xSize и m_zSize. Несмотря на двухмерный характер сетки, для хранения координат вершин мы используем линейный (одномерный) контейнер m_cPoints, так как это существенно упрощает объявление контейнера и работу с ним. В частности, упрощаются файловые операции. Выбор четырех смежных точек генерируемого примитива (например, GL_QUADS) происходит с помощью четырех индексов (n, i, j, k). Индекс п последовательно пробегает по всем вершинам в порядке слева направо. Более точно алгоритм перебора вершин можно определить так: сначала проходим по сетке узлов вдоль оси X при Z = 0, затем увеличиваем Z и вновь проходим вдоль X и т. д. Индексы i, j, k вычисляются относительно индекса п. В ветви связанных четырехугольников (GL_QUAD_STRIP) работают только два индекса.
В контейнер m_cPoints данные попадают после того, как они будут прочитаны из файла. Для того чтобы при открытии приложения в его окне уже находился график функции, необходимо заранее создать файл с данными по умолчанию, открыть и прочесть его содержимое. Это будет сделано в коде функций
DefaultGraphic и SetGraphPoints. Алгоритм функции DrawScene разработан в предположении, что контейнер точек изображаемой поверхности уже существует. Флаг m_bQuad используется для выбора способа создания полигонов: в виде отдельных (GL_QUADS) или связанных (GL_QUAD_STRIP) четырехугольников. Позднее мы введем команду меню для управления этой регулировкой:
void COGView: : DrawScene ()
{
//====== Создание списка рисующих команд
glNewList (1, 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
//====== Связанные полигоны начинаются
//====== на каждой полосе вновь
if (!m_bQuad)
glBegin (GLJ2UAD_STRIP) ;
//====== Цикл прохода вдоль оси X
for (UINT x=0; x
// i, j, k, n — 4 индекса вершин примитива при
// обходе в направлении против часовой стрелки
int j = i + m_xSize, // Индекс узла с большим Z
k = j+1/ // Индекс узла по диагонали
n = i+1; // Индекс узла справа
//=== Выбор координат 4-х вершин из контейнера
float
xi = m_cPoints [i] .x,
yi = m_cPoints [i] .у,
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;
//====== Задание вектора нормали
glNorma!3f (vx,vy,vz);
// Ветвь создания несвязанных четырехугольников
if (m_bQuad)
{
//==== Обход вершин осуществляется
//==== в направлении против часовой стрелки
glColorSf (0.2f, 0.8f, l.f);
glVertexSf (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) ;
glColor3f (0.7f, 0.8f, l.f);
glVertexSf (xn, yn, zn) ;
}
else
//==== Ветвь создания цепочки четырехугольников
{
glColor3f (0.9f, 0.9f, l.0f);
glVertexSf (xn, yn, zn) ;
glColorSf (0.5f, 0.8f, l.0f);
glVertexSf (xj, у j , zj);
//====== Закрываем блок команд GL_QUAD_STRIP
if (!m_bQuad) glEnd ( ) ; } //====== Закрываем блок команд GL_QUADS
if (m_bQuad)
glEnd() ;
// ====== Закрываем список команд OpenGL
glEndList() ;
}
При анализе кода обратите внимание на тот факт, что вектор нормали вычисляется по упрощенной формуле, так как линии сетки узлов, над которой расположены вершины поверхности, параллельны осям координат (X, Z). В связи с этим равны нулю компоненты az и bх векторов боковых сторон в формуле для нормали (см. раздел «Точное вычисление нормалей» в предыдущем уроке).
Подготовка окна
Подготовка окна
Вы помните, что подготовку контекста передачи OpenGL надо рассматривать как некий обязательный ритуал, в котором порядок действий определен. В этой процедуре выделяют следующие шаги:
Как было отмечено ранее, окнам, которые в своей клиентской области используют контекст передачи OpenGL, при создании следует задать биты стиля WS_CLIPCHILDREN и ws_CLiPSiBLiNGS. Сделайте это внутри существующего тела функции PreCreateWindow класса cocview, добавив нужные биты стиля к тем, что устанавливаются в заготовке:
BOOL COGView::PreCreateWindow(CREATESTRUCT& cs)
{
//====== Добавляем биты стиля, нужные OpenGL
cs.style |= WS_CLIPSIBLINGS | WS_CLIPCHILDREN;
return CView::PreCreateWindow(cs);
}
Вы помните, что окно OpenGL не должно позволять Windows стирать свой фон, так как данная операция сильно тормозит работу конвейера. В связи с этим введите в функцию обработки WM_ERASEBKGND код, сообщающий системе, что сообщение уже обработано:
BOOL COGView::OnEraseBkgnd(CDC* pDC)
{
return TRUE;
}
Окно OpenGL имеет свой собственный формат пикселов. Нам следует выбрать и установить подходящий формат экранной поверхности в контексте устройства HDC, а затем создать контекст передачи изображения (HGLRC). Для описания формата пикселов экранной поверхности используется структура PIXELFORMATDESCRIPTOR. Выбор формата зависит от возможностей карты и намерений разработчика. Мы зададим в полях этой структуры такие настройки:
В функцию OnCreate введите код подготовки окна OpenGL. Работа здесь ведется со структурой PIXELFORMATDESCRIPTOR. Кроме того, в ней создается контекст m_hRC и устанавливается в качестве текущего:
int COGView::OnCreate(LPCREATESTROCT IpCreateStruct)
{
if (CView::OnCreate(IpCreateStruct) == -1)
return -1;
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, // Глубина 2-буфера
0, // Глубина буфера Stencil
0, // Глубина буфера Auxiliary
0, // Теперь игнорируется
0, // Количество плоскостей
0, // Теперь игнорируется
0, // Цвет прозрачной маски
0 // Теперь игнорируется };
//====== Добываем дежурный контекст
m_hdc = ::GetDC(GetSafeHwnd());
//====== Просим выбрать ближайший совместимый формат
int iD = ChoosePixelForraat(m_hdc, spfd);
if ( !iD )
{
MessageBoxC'ChoosePixelFormat: :Error") ;
return -1;
}
//====== Пытаемся установить этот формат
if ( ISetPixelFormat (m_hdc, iD, Spfd) )
{
MessageBox("SetPixelFormat::Error");
return -1;
}
//====== Пытаемся создать контекст передачи OpenGL
if ( !(m_hRC = wglCreateContext (m_hdc)))
{
MessageBox("wglCreateContext::Error");
return -1;
}
//====== Пытаемся выбрать его в качестве текущего
if ( IwglMakeCurrent (m_hdc, m_hRC))
{
MessageBox("wglMakeCurrent::Error");
return -1;
//====== Теперь можно посылать команды OpenGL
glEnable(GL_LIGHTING); // Будет освещение
//====== Будет только один источник света
glEnable(GL_LIGHTO);
//====== Необходимо учитывать глубину (ось Z)
glEnable(GL_DEPTH_TEST);
//====== Необходимо учитывать цвет материала поверхности
glEnable(GL_COLOR_MATERIAL);
//====== Устанавливаем цвет фона .
SetBkColor () ;
//====== Создаем изображение и запоминаем в списке
DrawScene () ;
return 0;
}
Контекст передачи (rendering context) создается функцией wglCreateContext с учетом выбранного формата пикселов. Так осуществляется связь OpenGL с Windows. Создание контекста требует, чтобы обычный контекст существовал и был явно указан в параметре wglCreateContext. HGLRC использует тот же формат пикселов, что и НОС. Мы должны объявить контекст передачи в качестве текущего (current) и лишь после этого можем делать вызовы команд OpenGL, которые производят включение некоторых тумблеров в машине состояний OpenGL. Вызов функции DrawScene, создающей и запоминающей изображение, завершает обработку сообщения. Таким образом, сцена рассчитывается до того, как приходит сообщение о перерисовке WM_PAINT. Удалять контекст передачи надо после отсоединения его от потока. Это делается в момент, когда закрывается окно представления. Введите в тело заготовки OnDestroy следующие коды:
void COGView::OnDestroy(void)
{
//====== Останавливаем таймер анимации
KillTimer(1);
//====== Отсоединяем контекст от потока
wglMakeCurrent(0, 0); //====== Удаляем контекст
if (m_hRC)
{
wglDeleteContext(m_hRC);
m_hRC = 0;
}
CView::OnDestroy() ;
}
Так же как и в консольном проекте OpenGL, обработчик сообщения WM_SIZE должен заниматься установкой прямоугольника просмотра (giviewport) и мы, так же как и раньше, зададим его равным всей клиентской области окна. -Напомним, что конвейер OpenGL использует эту установку для того, чтобы поместить изображение в центр окна и растянуть или сжать его пропорционально размерам окна. Кроме того, в обработке onSize с помощью матрицы проецирования (GL_PROJECTION) задается тип проекции трехмерного изображения на плоское окно. Мы выбираем центральный или перспективный тип проецирования и задаем при этом угол зрения равным m_AngleView. В конструкторе ему было присвоено значение в 45 градусов:
void COGView::OnSize(UINT nType, int ex, int cy)
{
//====== Вызов родительской версии
CView::OnSize(nType, ex, cy) ;
//====== Вычисление диспропорций окна
double dAspect = cx<=cy ? double(cy)/ex : double(ex)/cy;
glMatrixMode (GL_PROJECTION) ;
glLoadldentity() ;
//====== Установка режима перспективной проекции
gluPerspective (m_AngleView, dAspect, 0.01, 10000.);
//====== Установка прямоугольника просмотра
glViewport(0, 0, сх, су);
}
Работа с контейнером
Работа с контейнером
Для работы с файлом мы пользовались буфером переменных типа BYTE. Для работы с данными в памяти значительно более удобной структурой данных является динамический контейнер. Мы, как вы помните, выбрали для этой цели контейнер, скроенный по шаблону vector. При заказе на его изготовление указали тип данных для хранения в контейнере. Это объекты класса CPointSD (точки трехмерного пространства). Мы пошли по простому пути и храним в файле только один компонент Y из трех координат точек поверхности в 3D. Остальные две координаты (узлов сетки на плоскости X-Z) будем генерировать на регулярной основе. Такой подход оправдан тем, что изображение OpenGL все равно претерпевает нормирующие преобразования, перед тем как попасть на двухмерный экран. Создание контейнера точек производится в теле функции SetGraphPoints, к разработке которой сейчас и приступим.
На вход функции подается временный буфер (и его размер), в который попали данные из файла. В настоящий момент в буфере находятся данные тестовой поверхности, а потом, при вызове из функции ReadData, в него действительно попадут данные из файла. Выбор данных из буфера происходит аналогично их записи. Здесь мы пользуемся адресной арифметикой, определяемой типом указателя. Так, операция ++ в применении к указателю типа UINT сдвигает его в памяти на sizeof (UINT) байт. Смена типа указателя (на float*) происходит в тот момент, когда выбраны данные о размерах сетки узлов.
Для надежности сначала проверяем данные из буфера на внутреннюю непротиворечивость в смысле размерностей. Затем мы уничтожаем данные контейнера и генерируем новые на основе содержимого буфера. В процессе генерации трехмерных координат точек их ординаты (Y) масштабируются для того, чтобы график имел пропорции, удобные для просмотра:
void COGView::SetGraphPoints(BYTE* buff, DWORD nSize)
{
//====== Готовимся к расшифровке данных буфера
//====== Указываем на него указателем целого типа
UINT *p = (UINT*)buff;
//=== Выбираем данные целого типа, сдвигая указатель
m_xSize = *р; m_zSize = *++p;
//====== Проверка на непротиворечивость
if (m_xSize<2 || m_zSize<2 ||
m_xSize*m_zSize*sizeof(float)
+ 2 * sizeof(UINT) != nSize)
{
MessageBox (_T ("Данные противоречивы") ) ;
return;
}
//====== Изменяем размер контейнера
//====== При этом его данные разрушаются
m_cPoints . resize (m_xSize*m_zSize) ;
if (m_cPoints .empty () )
{
MessageBox (_T ("He возможно разместить данные")
return;
}
//====== Подготовка к циклу пробега по буферу
//====== и процессу масштабирования
float x, z,
//====== Считываем первую ординату
*pf = (float*) ++р,
fMinY = *pf,
fMaxY = *pf,
right = (m_xSize-l) /2 . f ,
left = -right,
read = (m_zSize-l) /2 . f ,
front = -rear,
range = (right + rear) /2. f;
UINTi, j, n;
//====== Вычисление размаха изображаемого объекта
m_fRangeY = range;
m_fRangeX = float (m_xSize) ;
m_fRangeZ = float (m_zSize) ;
//====== Величина сдвига вдоль оси Z
m_zTrans = -1.5f * m_fRangeZ;
//====== Генерируем координаты сетки (X-Z)
//====== и совмещаем с ординатами Y из буфера
for (z=front, i=0, n=0; i
for (x=left, j=0; j
MinMax (*pf, fMinY, fMaxY) ;
m_cPoints[n] = CPoint3D(x, z, *pf++) ;
}
}
//====== Масштабирование ординат
float zoom = fMaxY > fMinY ? range/ (fMaxY-fMinY)
: l.f;
for (n=0; n
m_cPoints [n] . у = zoom * (m_cPoints [n] . у - fMinY) - range/2. f;
}
}
При изменении размеров контейнера методом (resize) все его данные разрушаются. В двойном цикле пробега по узлам сетки мы восстанавливаем (генерируем заново) координаты X и Z всех вершин четырехугольников. В отдельном цикле пробега по всему контейнеру происходит масштабирование ординат (умножение на предварительно вычисленный коэффициент zoom). В используемом алгоритме необходимо искать экстремумы функции у = f (x, z). С этой целью удобно иметь глобальную функцию MinMax, которая корректирует значение минимума или максимума, если входной параметр превышает существующие на сей момент экстремумы. Введите тело этой функции в начало файла реализации оконного класса (ChildView.cpp):
inline void MinMax (float d, floats Min, float& Max)
{
//====== Корректируем переданные по ссылке параметры
if (d > Max)
Max = d; // Претендент на максимум
else if (d < Min)
Min = d; // Претендент на минимум
}
Реакции на сообщения Windows
Реакции на сообщения Windows
Вспомните, как вы ранее вводили в различные классы реакции на сообщения Windows и повторите эти действия для класса cOGView столько раз, сколько необходимо, чтобы в нем появились стартовые заготовки функций обработки следующих сообщений:
В конструктор класса вставьте код установки начальных значений переменных:
COGView::COGView()
{
//====== Контекст передачи пока отсутствует
m_hRC = 0;
//====== Начальный разворот изображения
m_AngleX = 35.f;
m_AngleY = 20.f;
//====== Угол зрения для матрицы проекции
m_AngleView = 45.f;
//====== Начальный цвет фона
m_BkClr = RGB(0, 0, 96);
// Начальный режим заполнения внутренних точек полигона
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;
//======
Реакция на сообщение о перерисовке
Реакция на сообщение о перерисовке
В функции перерисовки должна выполняться стандартная последовательность действий, которая стирает back-буфер и буфер глубины, корректирует матрицу моделирования, вызывает из списка команды рисования и по завершении рисования переключает передний и задний буферы. Полностью замените существующий текст функции OnDraw на тот, который приведен ниже: void COGView:: OnDtaw (CDC" pDC]
glClear
glMatrixMode(GLjtoDELVIEH) ;
glLoadldentitylT;
SetLight() ;
//=====Формировать
//===== Переключение буферов SwapBuffera
}
Рисуем четырехугольниками
Рисуем четырехугольниками
m_bQuad = true;
//====== Начальный значения параметров освещения
m_LightParam[0] = 50; // X position
m_LightParam[l] = 80; // Y position
m_LightParam[2] = 100; // Z position
m_LightParam[3] = 15; // Ambient light
m_LightParam[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
}
Вид той же поверхности но освещенной справа
Рисунок 7.6. Вид той же поверхности, но освещенной справа

Идентификаторы элементов управления
Таблица 7.1. Идентификаторы элементов управления
Элемент
Идентификатор
Диалог
IDD_PROP
Ползунок Ambient в группе Light
IDC_AMBIENT
Ползунок Diffuse в группе Light
IDC_DIFFUSE
Ползунок Specular в группе Light
IDC_SPECULAR
; Static Text справа от Ambient в группе Light
IDC_AMB_TEXT
, Static Text справа от Diffuse в группе Light
IDC_DIFFUSE_TEXT
Static Text справа от Specular в группе Light
IDC_SPECULAR_TEXT
Ползунок Ambient в группе Material
IDC_AMBMAT
Ползунок Diffuse в группе Material
IDC_DIFFMAT
' Ползунок Specular в группе Material
IDC_SPECMAT
f Static Text справа от Ambient в группе Material
IDC_AMBMAT_TEXT
:! Static Text справа от Diffuse. в группе Material
IDC_DIFFMATJFEXT
; Static Text справа от Specular в группе Material
IDC_SPECMAT_TEXT
Ползунок Shim'ness
IDC_SHINE
Ползунок Emission
IDC_EMISSION
« Static Text справа от Shininess
IDC_SHINE_TEXT
Static Text справа от Emission
IDC_EMISSION_TEXT
Ползунок X
IDC_XPOS
| Ползунок Y
IDC_YPOS
1 Ползунок Z
IDC_ZPOS
Static Text справа от X
IDC_XPOS_TEXT
Static Text справа от Y
IDC_YPOS_TEXT
Static Text справа от Z
IDC_ZPOS_TEXT
Кнопка Data File
IDC_FILENAME
Для управления диалогом следует создать новый класс. Для этого можно воспользоваться контекстным меню, вызванным над формой диалога.
Просмотрите объявление класса CPropDlg, которое должно появиться в новом окне PropDlg.h. Как видите, мастер сделал заготовку функции DoDataExchange для обмена данными с элементами управления на форме диалога. Однако она нам не понадобится, так как обмен данными будет производиться в другом стиле, характерном для приложений не MFC-происхождения. Такое решение выбрано в связи с тем, что мы собираемся перенести рассматриваемый код в приложение, созданное на основе библиотеки шаблонов ATL. Это будет сделано в уроке 9 при разработке элемента ActiveX, а сейчас введите в диалоговый класс новые данные. Они необходимы для эффективной работы с диалогом в немодальном режиме. Важным моментом в таких случаях является использование указателя на оконный класс. С его помощью легко управлять окном прямо из диалога. Мы слегка изменили конструктор и ввели вспомогательный метод GetsiiderNum. Изменения косметического характера вы обнаружите сами:
#pragma once
class COGView; // Упреждающее объявление
class CPropDlg : public CDialog
{
DECLARE_DYNAMIC(CPropDlg)
public:
COGView *m_pView; // Адрес представления
int m_Pos[ll]; // Массив позиций ползунков
CPropDlg(COGView* p) ;
virtual ~CPropDlg();
// Метод для выяснения ID активного ползунка int GetsiiderNum(HWND hwnd, UINT& nID) ;
enum { IDD = IDD_PROP };
protected: virtual void DoDataExchange(CDataExchange* pDX);
DECLARE_MESSAGE_MAP()
};
Откройте файл реализации диалогового класса и с учетом сказанного про адрес окна введите изменение в тело конструктора, который должен приобрести такой вид:
CPropDlg::CPropDlg(COGView* p)
: CDialog(CPropDlg::IDD, p)
{
//====== Запоминаем адрес объекта
m_pView = p;
}
При каждом открытии диалога все его элементы управления должны отражать текущие состояния регулировок (положения движков), которые хранятся в классе представления. Обычно эти установки производят в коде функции OninitDialog. Введите в класс CPropDlg стартовую заготовку этой функции (CPropDlg > Properties > Overrides > OninitDialog > Add) и наполните ее кодами, как показано ниже:
BOOL CPropDlg: rOnlnitDialog (void)
{ CDialog: :OnInitDialog () ;
//====== Заполняем массив текущих параметров света
m_pView->GetLightParams (m _Pos) ;
//====== Массив идентификаторов ползунков
UINT IDs[] =
{
IDC_XPOS, IDC_YPOS, IDC_ZPOS,
IDC_AMBIENT,
IDC_DIFFUSE,
IDC_SPECULAR,
IDC_AMBMAT,
IDC_DIFFMAT,
IDC_SPECMAT,
IDC_SHINE,
IDCEMISSION
//====== Цикл прохода по всем регуляторам
for (int i=0; Ksizeof (IDs) /sizeof (IDs [ 0] ) ; i++)
{
//=== Добываем Windows-описатель окна ползунка H
WND hwnd = GetDlgItem(IDs[i] } ->GetSafeHwnd () ;
UINT nID;
//====== Определяем его идентификатор
int num = GetSliderNum(hwnd, nID) ;
// Требуем установить ползунок в положение m_Pos[i]
: :SendMessage(hwnd, TBM_SETPOS, TRUE, (LPARAM) m_Pos [i] )
char s [ 8 ] ;
//====== Готовим текстовый аналог текущей позиции
sprintf (s, "%d" ,m_Pos [ i] ) ;
//====== Помещаем текст в окно справа от ползунка
SetDlgltemText (nID, (LPCTSTR) s) ;
}
return TRUE;
}
Вспомогательная функция GetsliderNum по переданному ей описателю окна (hwnd ползунка) определяет идентификатор связанного с ним информационного окна (типа Static text) и возвращает индекс соответствующей ползунку пози ции в массиве регуляторов:
int CPropDlg: :GetSliderNum (HWND hwnd, UINT& nID)
{
//==== GetDlgCtrllD по известному hwnd определяет
//==== и возвращает идентификатор элемента управления
switch ( : : GetDlgCtrllD (hwnd) )
{
// ====== Выясняем идентификатор окна справа
case IDC_XPOS:
nID = IDC_XPOS_TEXT;
return 0;
case IDC_YPOS:
nID = IDC_YPOS_TEXT;
return 1;
case IDC_ZPOS:
nID = IDC_ZPOS_TEXT;
return 2;
case IDC_AMBIENT:
nID = IDC_AMB_TEXT;
return 3;
case IDC_DIFFUSE:
nID = IDC_DIFFUSE_TEXT;
return 4 ;
case IDC_SPECULAR:
nID = IDC_SPECULAR_TEXT;
return 5; case IDC_AMBMAT:
nID = IDC_AMBMAT_TEXT;
return 6 ;
case IDC_DIFFMAT:
nID = IDC_DIFFMAT_TEXT;
return 7 ;
case IDC_SPECMAT:
nID = IDC_SPECMAT_TEXT;
return 8 ; case IDC_SHINE:
nID = IDC_SHINE_TEXT;
return 9;
case IDC_EMISSION:
nID = IDC_EMISSION_TEXT;
return 10;
}
return 0;
}
В диалоговый класс введите обработчики сообщений WM_HSCROLL и WM_CLOSE, a также реакцию на нажатие кнопки IDC_FILENAME. Воспользуйтесь для этого окном Properties и его кнопками Messages и Events. В обработчик OnHScroll введите логику определения ползунка и управления им с помощью мыши и клавиш. Подобный код мы подробно рассматривали в уроке 4. Прочтите объяснения вновь, если это необходимо, Вместе с сообщением WM_HSCROLL система прислала нам адрес объекта класса GScrollBar, связанного с активным ползунком. Мы добываем Windows-описатель его окна (hwnd) и передаем его в функцию GetsliderNum, которая возвращает целочисленный индекс. Последний используется для доступа к массиву позиций ползунков. Кроме этого, система передает nSBCode, который соответствует сообщению об одном из множества событий, которые могут произойти с ползунком (например, управление клавишей левой стрелки — SB_LINELEFT). В зависимости от события мы выбираем для ползунка новую позицию:
void CPropDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
//====== Windows-описатель окна активного ползунка
HWND hwnd = pScrollBar->GetSafeHwnd();
UINT nID;
//=== Определяем индекс в массиве позиций ползунков
int num = GetSliderNum(hwnd, nID) ;
int delta, newPos;
//====== Анализируем код события
switch (nSBCode)
{
case SBJTHUMBTRACK:
case SB_THUMBPOSITION: // Управление мышью
m_Pos[num] = nPos;
break; case SB_LEFT: // Клавиша Home
delta = -100;
goto New_Pos; case SB_RIGHT: // Клавиша End
delta = + 100;
goto New__Pos; case SB_LINELEFT: // Клавиша <-
delta = -1;
goto New_Pos; case SB_LINERIGHT: // Клавиша ->
delta = +1;
goto New_Pos; case SB_PAGELEFT: // Клавиша PgUp
delta = -20;
goto New_Pos; case SB_PAGERIGHT: // Клавиша PgDn
delta = +20-;
goto New_Pos;
New_Pos: // Общая ветвь
//====== Устанавливаем новое значение регулятора
newPos = m_Pos[num] + delta;
//====== Ограничения
m_Pos[num] = newPos<0 ? 0 :
newPos>100 ? 100 : newPos;
break; case SB ENDSCROLL:
default:
return;
}
//====== Синхронизируем текстовый аналог позиции
char s [ 8 ] ;
sprintf (s, "%d",m__Pos [num] ) ;
SetDlgltemText (nID, (LPCTSTR)s);
//---- Передаем изменение в класс COGView
m_pView->SetLightParam (num, m_Pos [num] ) ;
}
Рассматриваемый диалог используется в качестве панели управления освещением сцены, поэтому он должен работать в немодальном режиме. Особенностью такого режима, как вы знаете, является то, что при закрытии диалога он сам должен позаботиться об освобождении памяти, выделенной под объект собственного класса. Эту задачу можно решить разными способами. Здесь мы покажем, как это делается в функции обработки сообщения WM_CLOSE. До того как уничтожено Windows-окно диалога, мы обнуляем указатель m_pDlg, который должен храниться в классе COGView и содержать адрес объекта диалогового класса. Затем вызываем родительскую версию функции OnClose, которая уничтожает Windows-окно. Только после этого мы можем освободить память, занимаемую объектом своего класса:
void CPropDlg: :OnClose (void)
{
//=== Обнуляем указатель на объект своего класса
m_pView->m_pDlg = 0;
//====== Уничтожаем окно
CDialog: :OnClose () ;
//====== Освобождаем память
delete this;
}
Реакция на нажатие кнопки IDC_FILENAME совсем проста, так как основную работу выполняет класс COGView. Мы лишь вызываем функцию, которая реализована в этом классе:
void CPropDlg:: OnClickedFilename (void)
{
//=== Открываем файловый диалог и читаем данные
m_pView->ReadData ( ) ;
}
Создание немодального диалога должно происходить в ответ на выбор команды меню Edit > Properties. Обычно объект диалогового класса, используемого в немодальном режиме, создается динамически. При этом предполагается, что класс родительского окна хранит указатель m_pDlg на объект класса диалога. Значение указателя обычно используется не только для управления им, но и как признак его наличия в данный момент. Это позволяет правильно обработать ситуацию, когда диалог уже существует и вновь приходит команда о его открытии. Введите в класс COGView новую public-переменную:
CPropDlg *m_pDlg; // Указатель на объект диалога
В начало файла заголовков OGView.h вставьте упреждающее объявление класса
CPropDlg:
class CPropDlg; // Упреждающее объявление
В конструктор COGView вставьте обнуление указателя:
m_pDlg =0; // Диалог отсутствует
Для обеспечения видимости класса CPropDlg дополните список директив препроцессора файла OGView.cpp директивой:
linclude "PropDlg.h"
Теперь можно ввести коды функции, которая создает диалог и запускает его вызовом функции Create (в отличие от DoModal для модального режима). Если происходит попытка повторного открытия диалога, то возможны два варианта развития событий:
Реализуем первый вариант:
void COGView::OnEditProperties (void)
{
//====== Если диалог еще не открыт
if (!m_pDlg)
{
//=== Создаем его и запускаем в немодальном режиме
m_pDlg = new CPropDlg(this);
m_pDlg->Create(IDD_PROP);
}
else
// Иначе, переводим фокус в окно диалога
m_pDlg->SetActiveWindow();
}
Реакция на команду обновления пользовательского интерфейса при этом может быть такой:
void COGView::OnUpdateEditProperties(CCmdUI *pCmdUI)
{
pCmdUI->SetCheck (m_pDlg != 0);
}
Второй вариант потребует меньше усилий:
void COGView::OnEditProperties (void)
{
m_pDlg = new CPropDlg(this);
m_pDlg->Create(IDD_PROP); }
Но при этом необходима другая реакция на команду обновления интерфейса:
void COGView::OnUpdateEditProperties(CCmdUI *pCmdUI)
{
pCmdUI->Enable(m_pDlg == 0);
}
Выберите и реализуйте один из вариантов.
Завершая разработку приложения, вставьте в панель управления четыре кнопки
Для команд ID_EDIT_BACKGROUND, ID_EDIT_PROPERTIES, ID_VIEW_FILL И ID_VIEW_
QUAD. Заодно уберите из нее неиспользуемые нами кнопки с идентификаторами
ID_FILE_NEW, ID_FILE_OPEN, ID_FILE_SAVE, ID_FILE_PRINT, ID__EDIT_CUT,
ID_EDIT_COPY, ID_EDIT_PASTE. Запустите приложение, включите диалог Edit > Properties и попробуйте управлять регуляторами параметров света. Отметьте, что далеко не все из них отчетливым образом изменяют облик поверхности. Нажмите кнопку Data File, при этом должен открыться файловый диалог, но мы не сможем открыть никакого другого файла, кроме того, что был создан по умолчанию. Он имеет имя «Sin.dat» и должен находиться (и быть виден) в папке проекта. В качестве упражнения создайте какой-либо другой файл с данными, отражающими какую-либо поверхность в трехмерном пространстве. Вы можете воспользоваться для этой цели функцией DefaultGraphic, немного модифицировав ее код. На Рисунок 7.5 и 7.6 приведены поверхности, полученные таким способом. Вы можете видеть эффект, вносимый различными настройками параметров освещения.
Если вы тщательно протестируете поведение приложения, то обнаружите недостатки. Отметим один из них. Закрытые части изображения при некотором ракурсе просвечивают сквозь те части поверхности, которые находятся ближе к наблюдателю. Причину этого дефекта было достаточно трудно выявить. И здесь опять пришли на помощь молодые, талантливые слушатели Microsoft Authorized Educational Center (www.Avalon.ru) Кондрашов С. С. (scondor@rambler.ru) и Фролов Д. С. (dmfrolov@rambler.ru). Оказалось, что при задании типа проекции с помощью команды gluPerspective значения ближней границы фрустума не должны быть слишком маленькими:
gluPerspective (45., dAspect, 0.01, 10000.);
В нашем случае этот параметр равен 0.01. Замените его на 10. и сравните качество генерируемой поверхности.
Подведем итог. В этой главе мы:
Трехмерные графики функций
Трехмерные графики функций
В этой главе мы разработаем Windows-приложение, которое в контексте OpenGL изображает трехмерный график функции, заданной произвольным массивом чисел. Данные для графика могут быть прочтены из файла, на который указывает пользователь. Кроме этого, пользователь будет иметь возможность перемещать график вдоль трех пространственных осей, вращать его вокруг вертикальной и горизонтальной осей и просматривать как в обычном, так и скелетном режим. Регулируя параметры освещения поверхности, пользователь может добиться наибольшей реалистичности изображения, то есть усилить визуальный эффект трехмерного пространства на плоском экране.
Графики могут представлять собой результаты расчета какого-либо физического поля, например поверхности равной температуры, давления, скорости, индукции, напряжения и т. д. в части трехмерного пространства, называемой расчетной областью. Пользователь объекта должен заранее подготовить данные и записать их в определенном формате в файл. Объект по команде пользователя считывает данные, нормирует, масштабирует и изображает в своем окне, внедренном в окно приложения-клиента. Пользователь, манипулируя мышью, управляет местоположением и вращением графика, а открыв окно диалога Properties, изменяет другие его атрибуты.
Управление изображением с помощью мыши
Управление изображением с помощью мыши
Итак, мы собираемся управлять ориентацией изображения с помощью левой кнопки мыши. Перемещение курсора мыши при нажатой кнопке должно вращать изображение наиболее естественным образом, то есть горизонтальное перемещение должно происходить вокруг вертикальной оси Y, а вертикальное — вокруг горизонтальной оси X. Если одновременно с мышью нажата клавиша Ctrl, то мы будем перемещать (транслировать) изображение вдоль осей X и Y. С помощью правой кнопки будем перемещать изображение вдоль оси Z. Кроме того, с помощью левой кнопки мыши мы дадим возможность придать вращению постоянный характер. Для этого в обработчик WM_LBUTTONUP введем анализ на превышение квантом перемещения (m_dx, m_dy) некоторого порога чувствительности. Если он превышен, то мы запустим таймер, и дальнейшее вращение будем производить с его помощью. Если очередной квант перемещения ниже порога чувствительности, то мы остановим таймер, прекращая вращение. В обработке WM_MOUSEMOVE следует оценивать желаемую скорость вращения, которая является векторной величиной из двух компонентов и должна быть пропорциональна разности двух последовательных координат курсора. Такой алгоритм обеспечивает гибкое и довольно естественное управление ориентацией объекта. Начнем с обработки нажатия левой кнопки. Оно, очевидно, должно всегда останавливать таймер, запоминать факт нажатия кнопки и текущие координаты курсора мыши:
void COGView: :OnLButtonDown (UINT nFlags, CPoint point)
{
//====== Останавливаем таймер
KillTimer(1);
//====== Обнуляем кванты перемещения
m_dx = 0.f; m_dy = 0.f;
//====== Захватываем сообщения мыши,
//====== направляя их в свое окно
SetCapture ();
//====== Запоминаем факт захвата
m_bCaptured = true;
//====== Запоминаем координаты курсора
m_pt = point;
}
При нажатии на правую кнопку необходимо выполнить те же действия, что и при нажатии на левую, но дополнительно надо запомнить сам факт нажатия правой кнопки, с тем чтобы правильно интерпретировать последующие сообщения о перемещении указателя мыши и вместо вращения производить сдвиг вдоль оси Z:
void COGView::OnRButtonDown(UINT nFlags, CPoint point)
{
//====== Запоминаем факт нажатия правой кнопки
m_bRightButton = true;
//====== Воспроизводим реакцию на левую кнопку
OnLButtonDown(nFlags, point);
}
В обработчик отпускания левой кнопки мы вводим анализ на необходимость продолжения вращения с помощью таймера. В случае превышения порога чувствительности, запускаем таймер, сообщения от которого будут говорить, что надо продолжать вращение, поддерживая текущее значение скорости:
void COGView::OnLButtonUp(UINT nFlags, CPoint point)
{
//====== Если был захват,
if (m_bCaptured)
//=== то анализируем желаемый квант перемещения
//=== на превышение порога чувствительности
if (fabs(m_dx) > 0.5f || fabs(m_dy) > 0.5f)
//=== Включаем режим постоянного вращения
SetTimer(1,33,0);
else
//=== Выключаем режим постоянного вращения
KillTimer(1);
//====== Снимаем флаг захвата мыши
m_bCaptured = false;
//====== Отпускаем сообщения мыши
ReleaseCapture();
}
}
Отпускание правой кнопки должно просто отмечать факт прекращения перемещения вдоль оси Z и отпускать сообщения мыши для того, чтобы они работали на другие окна, в том числе и на наше окно-рамку. Если этого не сделать, то станет невозможным использование меню главного окна. Проверьте, если хотите. Для этого достаточно закомментировать вызов функции ReleaseCapture в обеих функциях:
void COGView::OnRButtonUp(UINT nFlags, CPoint point)
{
//====== Правая кнопка отпущена
m_bRightButton = false;
//====== Снимаем флаг захвата мыши
m_bCaptured = false;
//====== Отпускаем сообщения мыши
ReleaseCapture();
}
Теперь реализуем самую сложную часть алгоритма — реакцию на перемещение курсора. Здесь мы должны оценить желаемую скорость вращения. Она зависит от того, насколько резко пользователь подвинул объект, то есть оценить модуль разности двух последних позиций курсора, В этой же функции надо выделить случай одновременного нажатия служебной клавиши Ctrl Если она нажата, то интерпретация движения мыши при нажатой левой кнопке изменяется. Теперь вместо вращения мы должны сдвигать объект, то есть пропорционально изменять переменные m_xTrans и m_yTrans, которые затем подаются на вход функции glTranslate. Третья ветвь алгоритма обрабатывает движение указателя при нажатой правой кнопке. Здесь необходимо изменять значение переменной m_zTrans, обеспечивая сдвиг объекта вдоль оси Z. Числовые коэффициенты пропорциональности, которые вы видите в коде функции, влияют на чувствительность мыши и подбираются экспериментально. Вы можете изменить их на свой вкус так, чтобы добиться желаемой управляемости изображения:
void COGView::OnMouseMove(UINT nFlags, CPoint point)
{
if (m_bCaptured) // Если был захват,
{
// Вычисляем компоненты желаемой скорости вращения
m_dy = float (point .у - m_pt .у) /40 . f ;
m_dx = float (point .x - m_pt .x) /40. f ;
//====== Если одновременно была нажата Ctrl,
if (nFlags & MK_CONTROL)
{
//=== Изменяем коэффициенты сдвига изображения
m_xTrans += m_dx;
m_yTrans -= m_dy;
}
else
{
//====== Если была нажата правая кнопка
if (m_bRightButton)
//====== Усредняем величину сдвига
m_zTrans += (m_dx + m_dy)/2.f;
else
{
//====== Иначе, изменяем углы поворота
m_AngleX += m_dy;
m_AngleY += m_dx;
}
}
//=== В любом случае запоминаем новое положение мыши
m_pt = point; Invalidate (FALSE) ;
}
}
Запустите и проверьте управляемость объекта. Введите коррективы чувствительности на свой вкус. Попробуйте скорректировать эффект влияния поворота вокруг оси X на интерпретацию знака желаемого вращения вокруг оси Y. Здесь можно воспользоваться стеком матриц моделирования. Теперь добавим код в заготовку функции реакции на сообщения таймера с тем, чтобы ввести фиксацию состояния вращения.
Установка цвета фона
Установка цвета фона
Введите вспомогательную функцию, которая позволяет вычислить и изменить цвет фона окна OpenGL. Позже мы введем возможность выбора цвета фона с помощью стандартного диалога Windows по выбору цвета:
void COGView: :SetBkColor ()
{
//====== Расщепление цвета на три компонента
GLclampf red = GetRValue (m_BkClr) /255 . f ,
green = GetGValue (m_BkClr) /255. f ,
blue = GetBValue(m_BkClr) /255. f ;
//====== Установка цвета фона (стирания) окна
glClearColor (red, green, blue, 0.f);
//====== Непосредственное стирание
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) ;
}
Вид окна диалога по управлению параметрами света
Рисунок 7.4. Вид окна диалога по управлению параметрами света
Обратите внимание на то, что справа от каждого движка расположен элемент типа static Text, в окне которого будет отражено текущее положение движка в числовой форме. Три регулятора (элемента типа Slider Control) в левом верхнем углу окна диалога предназначены для управления свойствами света. Группа регуляторов справа от них поможет пользователю изменить координаты источника света. Группа регуляторов, объединенная рамкой (типа Group Box) с заголовком Material, служит для изменения отражающих свойств материала. Кнопка с надписью Data File позволит пользователю открыть файловый диалог и выбрать файл с данными для нового изображения. Для диалогов, предназначенных для работы в немодальном режиме, необходимо установить стиль Visible. Сделайте это в окне Properties > Behavior. Идентификаторы элементов управления мы сведем в табл. 7.1.
Вид освещенной поверхности в 3D
Рисунок 7.1. Вид освещенной поверхности в 3D
Вид поверхности освещенной слева
Рисунок 7.5. Вид поверхности, освещенной слева
Вид поверхности при использовании режима GL_QUAD_STRIP
Рисунок 7.2. Вид поверхности при использовании режима GL_QUAD_STRIP
Обработку следующей команды меню мы проведем в том же стиле, за исключением того, что переменная m_FillMode не является булевской, хоть и принимает лишь два значения (GL_FILL и GL_LINE). Из материала предыдущей главы помните, возможен еще одни режим изображения полигонов — GL_POINT. Логику его реализации при желании вы введете самостоятельно, а сейчас введите коды двух функции обработки команды меню:
void COGView::OnViewFill(void)
{
//=== Переключаем режим заполнения четырехугольника
m_FillMode = m_FillMode==GL_FILL ? GL_LINE : GL__FILL;
//====== Заново создаем изображение
DrawScene();
Invalidate(FALSE);
UpdateWindow() ;
}
void COGView::OnUpdateViewFill(CCmdUI *pCmdUI)
{
//====== Вставляем или убираем маркер выбора
pCmdUI->SetCheck(m_FillMode==GL_FILL) ;
}
Запустите и проверьте работу команд меню. Отметьте, что формула учета освещения работает и в случае каркасного изображения примитивов (Рисунок 7.3).
Вид поверхности созданной в режиме GL_LINE
Рисунок 7.3. Вид поверхности, созданной в режиме GL_LINE
Для обмена с диалогом по управлению освещением нам понадобятся две вспомогательные функции GetLightParams и SetLightParam. Назначение первой из которых заполнить массив переменных, отражающих текущее состояние параметров освещения сцены OpenGL. Затем этот массив мы передадим в метод диалогового класса для синхронизации движков (sliders) управления. Вторая функция позволяет изменить отдельный параметр и привести его в соответствие с положением движка. Так как мы насчитали 11 параметров, которыми хотим управлять, то придется ввести в окно диалога 11 регуляторов, которым соответствует массив m_LightPaxam из 11 элементов. Массив уже помещен в класс COGView, нам осталось лишь задействовать его:
void COGView: :GetLightParams (int *pPos)
{
//====== Проход по всем регулировкам
for (int i=0; i
//====== Заполняем транспортный массив pPos
pPos[i] = m_LightParam[i] ;
void COGView: :SetLightParam (short Ip, int nPos)
{ //====== Синхронизируем параметр lp и
//====== устанавливаем его в положение nPos
m_LightParam[lp] = nPos;
//=== Перерисовываем представление с учетом изменений
Invalidate (FALSE) ;
}
Включаем анимацию
Включаем анимацию
Реакция на сообщение о том, что истек очередной квант времени в 33 миллисекунды (именно такую установку мы сделали в OnLButtonUp) выглядит очень просто. Увеличиваем углы поворота изображения на те кванты, которые вычислили в функции OnMouseMove и вызываем перерисовку окна. Так как при непрерывном вращении углы постоянно растут, то можно искусственно реализовать естественную их периодичность с циклом в 360 градусов. Однако с этой задачей успешно справляется OpenGL, и вы можете убрать код ограничения углов:
void COGView: :OnTimer (UINT nIDEvent)
{
//====== Если это был наш таймер
if (nIDEvent==l)
{
//====== Увеличиваем углы поворота
m_AngleX += m_dy;
m_AngleY += m_dx;
//====== Ограничители роста углов
if (m_AngleX > 360)
m_AngleX -= 360;
if (m_AngleX <-360)
m_AngleX += 360;
if (m_AngleY > 360)
m_AngleY -=360;
if (m_AngleY <-360)
m_AngleY +=360;
//====== Просим перерисовать окно
Invalidate(FALSE);
}
else
//=== Каркас приложения обработает другие таймеры
CView::OnTimer(nIDEvent);
}
Запустите и протестируйте приложение. Скорректируйте, если необходимо, коэффициенты чувствительности.
Вспомогательный класс
Вспомогательный класс
Нам вновь, как и в предыдущем уроке, понадобится класс, инкапсулирующий функциональность точки трехмерного пространства CPoint3D. Контейнер объектов этого класса будет хранить вершины изображаемой поверхности. В коде, который приведен ниже, присутствует слегка измененное по сравнению с предыдущим объявление класса CPoint3D, а также объявления новых данных и методов класса cocview. Заодно мы произвели упрощения стартового кода, которые обсуждались в уроке 5. Весь код введите в файл OGView.h вместо существующей в нем заготовки. Файл должен приобрести следующий вид1:
#pragma once
//========== Вспомогательный класс
class CPointSD
{
public: //====== Координаты точки
float x;
float у;
float z;
//====== Набор конструкторов
CPointSD ()
{
х = у - z = 0.f;
}
CPoint3D (float cl, float c2, float c3)
{
x = cl; z = c2; У = сЗ; ,
}
//====== Операция присвоения
CPoint3DS operator= (const CPointSDS pt)
x = pt.x; z = pt.z;
return *this;
У = pt.y;
//====== Конструктор копирования
CPointSD (const CPoint3D& pt)
{
*this = pt;
//=========== Класс окна OpenGL
class COGView :
public CView
{
protected:
COGView () ;
DECLARE_DYNCREATE(COGView)
public:
virtual ~COGView();
virtual void OnDraw(CDC* pDC) ;
virtual BOOL PreCreateWindow(CREATESTRUCT& cs) ,
//======= Новые данные класса
long m_BkClr; //
int m_LightParara[ll]; //
HGLRC m_hRC; //
HDC m_hdc; //
GLfloat m_AngleX; //
GLfloat m_AngleY; //
GLfloat m_AngleView; //
GLfloat m_fRangeX; //
GLfloat m_fRangeY; //
GLfloat m_fRangeZ; //
GLfloat m_dx; //
GLfloat m_dy; //
GLfloat m_xTrans; //
GLfloat m_yTrans; //
GLfloat m_zTrans; //
GLenura m_FillMode; //
bool m_bCaptured; //
bool m_bRightButton; //
bool m_bQuad; //
CPoint m_pt; //
UINT m_xSize; //
UINT m_zSize; //
//====== Массив вершин поверхности
vector
//====== Новые методы класса
//=-==== Подготовка изображения
void DrawScene();
Цвет фона окна Параметры освещения Контекст OpenGL Контекст Windows Угол поворота вокруг оси X Угол поворота вокруг оси Y Угол перспективы Размер объекта вдоль X Размер объекта вдоль Y Размер объекта вдоль Z Квант смещения вдоль X Квант смещения вдоль Y Смещение вдоль X Смещение вдоль Y Смещение вдоль Z Режим заполнения полигонов Признак захвата мыши Флаг правой кнопки мыши Флаг использования GL_QUAD Текущая позиция мыши Текущий размер окна вдоль X Текущий размер окна вдоль Y
//====== Создание графика по умолчанию
void DefaultGraphic();
//====== Создание массива по данным из буфера
void SetGraphPoints(BYTE* buff, DWORD nSize);
//====== Установка параметров освещения
void SetLight();
//====== Изменение одного из параметров освещения
void SetLightParam (short lp, int nPos);
//====== Определение действующих параметров освещения
void GetLightParams(int *pPos); //====== Работа с файлом данных
void ReadData();
//====== Чтение данных из файла
bool DoRead(HANDLE hFile);
//====== Установка Работа с файлом данных
void SetBkColor();
DECLARE MESSAGE MAP()
Ввод новых команд
Ввод новых команд
Вы заметили, что до сих пор обходились без каких-либо ресурсов. Мы не учитываем традиционный диалог About, планку меню главного окна, панель инструментов, две таблицы (строк и ускорителей) и два значка, которые присутствовали в каркасе приложения изначально. Дальнейшее развитие потребует ввести новые ресурсы. Главным из них будет диалог, который мы запустим в немодальном режиме и который позволит подробно исследовать влияние параметров освещения на качество изображения. Начинать, как обычно, следует с команд меню. Скорректируйте меню главного окна так, чтобы в нем появились новые команды:
Одновременно удалите не используемые нами команды: File > New, File > Open, File > Save, File > Save as, File > Recent File, Edit > Undo, Edit > Cut, Edit > Copy и Edit > Paste.
Примечание 1
Примечание 1
Вы, конечно, знаете, что идентификаторы команд можно не задавать. Они генерируются автоматически при перемещении фокуса от вновь созданной команды к любой другой.
После этого в классе cocview создайте обработчики всех новых команд с именами по умолчанию (их предлагает Studio.Net). При создании реакций на эти команды меню (COGView > Properties > Events) предварительно раскройте все необходимые элементы в дереве Properties t Commands. Одновременно с функциями обработки типа COMMAND создайте (для всех команд, кроме Edit > Background) функции обновления пользовательского интерфейса, то есть функции обработки типа UPDATE_ COMMANDJJI. Они, как вы помните, следят за состоянием команд меню и соответствующих им кнопок панели управления, обновляя интерфейс пользователя. Команды становятся доступными или, наоборот, в зависимости признака, управляемого програмистом.
В обработчике OnEditBackground мы вызовем стандартный диалог по выбору цвета, сразу открыв обе его страницы (см. флаг CC_FULLOPEN). С помощью этого диалога пользователь сможет изменить цвет фона:
void COGView::OnEditBackground(void)
{
//====== Создаем объект диалогового класса
CColorDialog dig(m_BkClr); //====== Устанавливаем бит стиля
dig.m_cc.Flags |= CC_FULLOPEN;
//====== Запускаем диалог и выбираем результат
if (cilg.DoModal ()==IDOK)
{
m_BkClr = dig.m_cc.rgbResuit;
//====== Изменяем цвет фона
SetBkColor();
Invalidate(FALSE);
}
}
Проверьте результат, запустив приложение и вызвав диалог. При желании создайте глобальный массив с 16 любимыми цветами и присвойте его адрес переменной lpCustColors, которая входит в состав полей структуры m_сс, являющейся членом класса CColorDialog. В этом случае пользователь сможет подобрать и запомнить некоторые цвета.
В обработчик OnViewQuad введите коды, инвертирующие булевский признак m_bQuad, который мы используем как флаг необходимости рисования отдельными четырехугольниками (GL_QUADS), и заново создают изображение. Если признак инвертирован, то мы рисуем полосами (GL_QUAD_STRIP):
void COGView::OnViewQuad(void)
{
// Инвертируем признак стиля задания четырехугольников
m_bQuad = ! m_bQuad;
//====== Заново создаем изображение
DrawScene (); Invalidate(FALSE); UpdateWindow();
}
В обработчик команды обновления интерфейса введите коды, которые обеспечивают появление маркера выбора рядом с командой меню (или залипания кнопки панели управления):
void COGView::OnUpdateViewQuad(CCmdUI* pCmdUI)
{
//====== Вставляем или убираем маркер (пометку)
pCmdUI->SetCheck(m_bQuad==true);
}
Проверьте результат и попробуйте объяснить зубчатые края поверхности (Рисунок 7.2). Не знаю, правильно ли я поступаю, когда по ходу изложения вставляю задачи подобного рода. Но мной движет желание немного приоткрыть дверь в кухню разработчика и показать, что все не так уж просто. Искать ошибки в алгоритме, особенно чужом, является очень кропотливым занятием. Однако совершенно необходимо приобрести этот навык, так как без него невозможна работа в команде, а также восприятие новых технологий, раскрываемых в основном посредством анализа содержательных (чужих) примеров (Samples). Чтобы обнаружить ошибку подобного рода, надо тщательно проанализировать код, в котором создается изображение (ветвь GL_QUAD_STRIP), и понять, что неправильно выбран индекс вершины. Замените строку givertex3f (xn, yn, zn); HaglVertexSf (xi, yi, zi); и вновь проверьте работу приложения. Зубчатость края должна исчезнуть, но в алгоритме, тем не менее, осталась еще небольшая, слабо заметная неточность. Ее обнаружение и исправление я оставляю вам, дорогой читатель.
От сырых COM API к проекту ATL
Библиотека типов
Библиотека типов
Для того чтобы клиенты, разработанные на других языках программирования, могли управлять объектами сервера, они должны иметь информацию о типах данных, используемых сервером при передаче параметров. Одним из способов получения этой информации является создание сервером библиотеки типов. Возвращаясь к файлам, которые сгенерировал компилятор MIDL, отметим, что он создает еще один (двоичный) TLB-файл (Type Library). После успешной компиляции вы можете обнаружить его в папке Debug. COM использует этот файл для реализации маршалинга, управляемого данными, который происходит на этапе выполнения программы. Двоичный TLB-файл воспринимается клиентом, написанным на одном из СОМ-совместимых языков. Например, его использует программа просмотра объектов Microsoft Excel. Инструмент Studio.Net ClassWizard умеет по информации из библиотеки типов создать классы, которые могут обращаться к свойствам и методам объектов. Программа на Visual Basic осуществляет раннее связывание на основе данных из библиотеки типов. Сведения о библиотеке типов также заносятся в реестр в специальный подраздел TypeLib в разделе HKEY_CLASSES_ROOT.
Двойственные интерфейсы
Двойственные интерфейсы
Технология Automation, ранее известная как OLE Automation, дает совершенно другой способ вызова клиентом методов, экспонируемых сервером, чем тот стандартный для СОМ способ, который мы уже рассмотрели. Вы помните, что он использует таблицу виртуальных указателей vtable на интерфейсы. Automation же использует стандартный СОМ-интерфейс IDispatch для доступа к интерфейсам. Поэтому говорят, что любой объект, поддерживающий IDispatch, реализует Automation. Также говорят о дуальном интерфейсе, имея в виду, что он может быть вызван как с помощью естественного способа (vtable), так и с помощью вычурного способа Automation. Итак, интерфейс IOpenGL предоставляет своим пользователям двойственный (dual) интерфейс.
Dual Interface понадобился для того, чтобы VBScript-сценарий мог использовать СОМ-объекты, созданные с помощью Visual C++. Клиенты, созданные на языке C++, могут с помощью Querylnterf асе получить адрес интерфейса и прямо вызывать его методы, пользуясь таблицей виртуальных функций (vtable), например:
p->SomeMethod(i, d);
В VBScript будут проблемы. Там нет строгого контроля соответствия типов и многие типы C++ ему неизвестны. Интерфейс IDispatch служит посредником в разговоре двух произведений Microsoft. Теперь программа на VBScript может добраться до метода SomeMethod, выполнив длинную цепь вызовов. Сначала она должна получить указатель на интерфейс IDispatch, затем с его помощью (GetiDsOf Names) узнать индекс желаемого метода (типа DISPID — dispatch identifier), на сей раз не 128-битный. После этого она сможет заставить систему выполнить коды метода SomeMethod, но не прямо, а с помощью метода IDispatch: : Invoke, который требует задать 8 параметров, смысл которых может приблизительно соответствовать следующему списку описаний. Последующий текст воспринимайте очень серьезно, так как он взят прямо из справки IDispatch:: invoke:
(Поток сознания в скобках, по Джойсу или Жванецкому: новые концепции, новые технологии, глубина мыслей, отточенность деталей, настоящая теория должна быть красивой, тупиковая ветвь?, монополисты не только заставляют покупать, но и навязывают свой способ мышления, что бы ты делал без MS, о чем думал, посмотри CLSID в реестре, видел ли я полезный элемент ActiveX, нужно ли бесшовно внедрять что-нибудь во что-нибудь, посмотри Interfaces в реестре, что лучше, Stingray-класс или внедренная по стандарту OLE таблица Excel, тонкий (thin) клиент не будет иметь кода, но будет иметь много картинок и часто покупать дешевые сеансы обслуживания, как раньше билеты в кино или баню, если не поддерживать обратную совместимость, то кто будет покупать, лучше не купить, чем перестать играть в DOS-игры, стройный (slim) клиент, хочешь, еще посчитаю — плати доллар, перестань думать, пора работать.)
Дуальные или интерфейсы диспетчеризации (dispinterfaces) в отличие от тех vtable-интерфейсов, с которыми вы уже знакомы, были разработаны для того, чтобы реализовать позднее связывание (late-binding) клиента с сервером. Инструментальная среда разработки Visual Basic в этом смысле является лидером, так как в ней вы почти без усилий можете создать приложение, способное на этапе выполнения, то есть поздно, получить информацию от объекта и пользоваться методами интерфейсов, информация о которых стала доступной благодаря IDispatch.
Возвращаясь к нашему проекту, отметим, что интерфейс юрепсъ предоставляет своим пользователям два одноименных метода FillColor. Первый метод позволяет пользователю изменить (propput) стандартное или встроенное (stock property) свойство: «цвет заливки». Второй — узнать (propget) текущее значение этого свойства. Этот интерфейс был вставлен мастером потому, что при создании элемента мы указали на -необходимость введения в него одного из стандартных свойств. С этой же целью мастер ввел в состав класса переменную:
OLE_COLOR m_clrFillColor;
которая будет хранить значение свойства. Мы должны ею управлять, поэтому давайте зададим начальное значение цвета в конструкторе класса. Найдите его и измените:
COpenGL()
{
m_clrFillColor = RGB (255,230,255);
}
Но этого мало. Для того чтобы увидеть результат, надо изменить коды функции рисования, которую вы найдете в том же файле OpenGLh.
Примечание 1
Примечание 1
Вступив в царство ATL, придется отречься от многих привычек, приобретенных в MFC. Вы уже заметили, что мы теперь вместо char* или CString пользуемся OLESTR, а вместо COLORREF— OLE_COLOR. Это еще не так отвлекает, но вот теперь надо рисовать без помощи привычного класса CDC и вернуться к описателю НОС контекста устройства, которым мы пользовались при разработке традиционного Windows-приложения на основе функций API. Также придется привыкнуть к тому, что описатель HOC hdcDraw упрятан в структуру типа ATL_DRAWINFO, ссылку на которую мы получаем в параметре метода OnDraw класса CComControl.
Напомню, что вся функциональность класса CComControl унаследована нашим классом COpenGL, который, кроме него, имеет еще 17 родителей. Состав полей структуры ATL_DRAWINFO не будем приводить здесь, чтобы не усугублять головокружение, а вместо этого предложим убедиться в том, что можно влиять на облик СОМ-объекта. Особенностью перерисовки СОМ-объекта является то, что он изображает себя в чужом окне. Поэтому, получив контекст устройства, связанный с этим окном, он должен постараться не рисовать вне пределов прямоугольника, отведенного для него. В Windows существует понятие поврежденной области окна (clip region). Это обычно прямоугольная область, в пределах которой система позволяет приложению рисовать. Если рисующие функции GDI попробуют выйти за границы этой области, то система не отобразит этих изменений. Следующий код интенсивно работает с clip region, поэтому для понимания алгоритма рекомендуем получить справку о функциях GetClipRgn и SelectClipRgn. Введите изменения в уже существующее тело функции OnDraw так, чтобы она приобрела вид:
HRESULT OnDraw(ATL_DRAWINFO& di)
{
//===== Преобразование RECTL в RECT
RECT& r = *(RECT*)di.prcBounds;
//===== Запоминаем текущую поврежденную область
HRGN hRgnOld = 0;
//== Функция GetClipRgn может возвратить: 0, 1 или -1
if (GetClipRgn(di.hdcDraw, hRgnOld) != 1) hRgnOld = 0;
//====== Создание новой области
HRGN hRgnNew = CreateRectRgn(r.left,r.top, r.right,r.bottom);
// Оптимистический прогноз (новая область воспринята)
bool bSelectOldRgn = false;
//=== Устанавливаем поврежденную область равной г
if (hRgnNew)
{
bSelectOldRgn = SelectClipRgn(di.hdcDraw,hRgnNew) == ERROR;
}
//=== Изменяем цвет фона и обрамляем объект
::rSelectObject(di.hdcDraw,
::CreateSolidBrush(m_clrFillColor)); Rectangle(di.hdcDraw, r.left, r.top,r.right,r.bottom);
//=== Параметры выравнивания текста и сам текст
SetTextAlign(di.hdcDraw, TA_CENTER | TA_BASELINE);
LPCTSTR pszText = _T("ATL 4.0 : OpenGL");
//=== Вывод текста в центр прямоугольника
TextOut(di.hdcDraw, (r.left + r.right)/2,
(r.top + r.bottom)/2,
pszText,Istrlen(pszText));
//=== Если был сбой, то устанавливаем старую область
if (bSelectOldRgn)
SelectClipRgn(di.hdcDraw, hRgnOld);
return S_OK;
}
В этой реализации функции OnDraw мы намеренно пошли на поводу у схемы, предложенной в заготовке. Структура RECTL, на которую указывает prcBounds, идентична структуре RECT, но при заливке она ведет себя на один пиксел лучше (см. справку). Здесь это никак не используется. Автору фрагмента не хотелось много раз писать выражение di. prcBounds->, поэтому он завел ссылку на объект типа RECTL, приведя ее к типу RECT. Здесь хочется «взять в руки» CRect, cstring и переписать фрагмент заново в более компактной форме, однако если вы попробуете это сделать, то получите сообщения о том, что CRect и cstring — неизвестные сущности. Они из другого царства MFC. Мы можем подключить поддержку MFC, но при этом многое потеряем. Одной из причин создания ATL была неповоротливость объектов на основе MFC в условиях web-страниц. Мы не можем себе этого позволить, так как собираемся работать с трехмерной графикой. Поэтому надо привыкать работать по правилам Win32-API и классов СОМ.
Вновь запустите приложение и убедитесь в том, что нам удалось слегка подкрасить объект. Теперь исследуем функциональность, которую получили бесплатно при оформлении заказа у мастера.
Попробуем это исправить. Событие, заключающееся в том, что пользователь объекта изменил одно из его стандартных свойств, поддерживаемых страницами не менее стандартного диалога, будет обработано каркасом СОМ-сервера и при этом вызвана функция copenGL: :OnFillColorChanged, код которой мы не трогали. Сейчас там есть только одна строка:
ATLTRACE(_T ("OnFillColorChanged\n"));
которая в режиме отладки (F5) выводит в окно Debug Studio.Net текстовое сообщение. Внесите в тело этой функции изменения:
void OnFillColorChanged()
{
//====== Если выбран системный цвет,
if (m_clrFillColor & 0x80000000)
//====== то выбираем его по индексу
m_clrFillColor=::GetSysColor(m_clrFillColor & Oxlf); ATLTRACE(_T("OnFillColorChanged\n"));
}
Признаком выбора системного цвета является единица в старшем разряде m_clrFillColor. В этом случае цвет задан не тремя байтами (red, green, blue), a индексом в таблице системных цветов (см. справку по GetSysColor). Выделяя этот случай, мы выбираем системный цвет с помощью API-функции GetSysColor. Заодно подправим функцию перерисовки, чтобы убедиться, что объект нам подчиняется и мы умеем убирать лишний код:
HRESULT OnDraw(ATL_DRAWINFO& di)
{
//====== Не будем преобразовывать в RECT
LPCRECTL р = di.prcBounds;
//====== Цвет подложки текста
::SetBkColor(di.hdcDraw,m_clrFillColor) ;
//====== Инвертируем цвет текста
::SetTextColor(di.hdcDraw, ~m_clrFillColor & Oxffffff);
//====== Цвет фона
::SelectObject(di.hdcDraw,
::CreateSolidBrush(m_clrFillColor));
Rectangle(di.hdcDraw, p->left, p->top, p->right, p->bottom);
SetTextAlign(di.hdcDraw, TA_CENTER | TA_BASELINE);
LPCTSTR pszText = _T("ATL 4.0 : OpenGL");
TextOut(di.hdcDraw, (p->left + p->right)/2,
(p->top + p->bottom)/2,
pszText, Istrlen(pszText)
};
return S_OK;
}
Запустите и убедитесь, что системные цвета выбираются корректно, а перерисовка при изменении размеров объекта не нарушает заданных границ. Некоторые проблемы возникают при инвертировании цвета фона, если он близок к нейтральному (128, 128, 128). В качестве упражнения решите эту проблему самостоятельно.
Фабрика классов
Фабрика классов
Логика функционирования нашего проекта (типа клиент-сервер ) вырождена, то есть излишне упрощена, так как мы хотели показать лишь основную нить алгоритма использования СОМ-объектов. Обычно в рамках этого алгоритма присутствует так называемая фабрика классов — специальный класс на стороне сервера, который реализует функциональность уже существующего и зарегистрированного в библиотеке СОМ интерфейса iciassFactory. Фабрики классов — это объекты СОМ создающие другие объекты сервера. Их цель — создать объект определенного типа, который однозначно задан своим CLSID. Каждый СОМ-объект должен в соответствии со стандартом иметь связанную с ним фабрику классов, которая ответственна за его создание. Так, в нашем случае мы должны иметь фабрику классов, способную воспроизводить любое требуемое клиентами количество объектов класса CoSay.
Интерфейс iciassFactory имеет всего два метода: Createlnstance и LockServer. Первый необходим для того, чтобы динамически создавать произвольное количество объектов тех классов (CLSID), которые живут в доме DLL СОМ-сервера, а второй — для того, чтобы запретить или разрешить системе выгружать сервер из памяти. Это позволяет пользователю гибко управлять необходимыми ресурсами. Если СОМ-объект пока не нужен клиентскому приложению, но вскоре может понадобиться, то, вызвав метод LockServer с параметром TRUE, клиент может запретить выгрузку из памяти DLL-сервера, несмотря на то что счетчик числа пользователей ее объектами равен нулю. Если в течение какого-то времени не предвидится использование СОМ-объектов, то клиент может вызвать метод LockServer с параметром FALSE, разрешив тем самым выгрузку DLL-сервера из памяти.
Для реализации этой функциональности вновь откройте проект СОМ-сервера My с от и в файл МуСоm.срр добавьте две глобальные переменные:
//====== Счетчик числа блокировок DLL
ULONG gLockCount;
//====== Счетчик числа пользователей СОМ-объектами
ULONG gObjCount;
В этот же файл введите новую функцию, которую будет экспортировать наша DLL:
STDAPI DllCanUnloadNow()
{
//====== Если счетчики нулевые, то мы позволяем
//====== системе выгрузку DLL-сервера
return !gLockCount && IgObjCount ? S_OK : S_FALSE;
}
В конструктор класса coSay добавьте код, увеличивающий счетчик числа пользователей объектом Со Say:
gObjCount++;
а в деструктор — уменьшающий:
gObjCount--;
Важным шагом, о котором, тем не менее, легко забыть, является своевременная коррекция файла MyCom.def. Вставьте в конец этого файла строку
DllCanUnloadNow PRIVATE
которая добавляет в список экспортируемых функций еще один элемент. В файл MyCom. h добавьте декларацию нового класса CoSayFactory, реализующего интерфейс iclassFactory. Отметьте, что он произведен от интерфейса iClassFactory, который, как и положено, имеет родителя I unknown. Вы помните, что на плечи класса ложится бремя реализации всех методов своих предков. По той же причине мы вновь заводим счетчик числа пользователей классом (m_ref):
//====== Фабрика классов СОМ DLL-сервера
class CoSayFactory : public IClassFactory
{
public:
CoSayFactory() ;
virtual ~CoSayFactory() ;
// lUnknown
HRESULT _stdcall Querylnterface(REFIID riid,
void** ppv);
UbONG _stdcall AddRefO; ULONG _stdcall Release();
// IClassFactory
HRESULT _stdcall Createlnstance(LPUNKNOWN pUnk,
REFIID riid, void** ppv);
HRESULT _stdcall LockServer(BOOL bLock); private:
ULONG m_ref; };
Реализацию тел заявленных методов вставьте в файл МуСоm.срр. Здесь мы вынуждены повторяться, вновь прокручивая логику управления временем жизни объектов СОМ:
//========== Фабрика классов
CoSayFactory::CoSayFactory()
{
m_ref = 0; gObjCount++;
}
CoSayFactory::-CoSayFactory()
{
gObjCount--;
}
//====== Методы lUnknown
HRESULT _stdcall CoSayFactory
::QueryInterface(REFIID riid, void** ppv)
{
*ppv = 0;
//=== На сей раз обойдемся без шаблона static_cast<>
if (riid == IID_IUnknown)
*ppv = (lUnknown*)this;
else if (riid == IID_IClassFactory)
*ppv = (IClassFactory*)this;
else
return E_NOINTERFACE;
AddRef();
return S_OK;
}
ULONG _stdcall CoSayFactory:rAddRef()
{
return ++m_ref;
}
ULONG _stdcall CoSayFactory::Release()
{
if (--m_ref==0)
delete this;
return m_ref;
//====== Методы интерфейса IClassFactory
HRESULT _ stdcall CoSayFactory: :CreateInstance
(LPUNKNOWN pUnk, REFIID riid, void** ppv)
{
// Этот параметр управляет аггрегированием
// объектов СОМ, которое мы не поддерживаем
if (pUnk)
return CLASS_E_NOAGGREGATION;
//== Создание нового объекта и запрос его интерфейса
CoSay *pSay = new CoSay;
HRESULT hr = pSay->Query!nterface (riid, ppv) ;
if (FAILED (hr))
delete pSay; return hr;
//=== Управление счетчиком фиксаций сервера в памяти
HRESULT _stdcall CoSayFactory::LockServer(BOOL bLock)
{
if (bLock) // Если TRUE, то увеличиваем счетчик
++gLockCount;
else // Иначе — уменьшаем
--gLockCount;
return S_OK;
}
Мы должны также изменить алгоритм функции DllGetciassOb j ect, которая теперь создает объект фабрики классов и запрашивает один из двух возможных интерфейсов (lUnknown, IClassFactory):
STDAPI DllGetClassObject (REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
if (rclsid != CLSID_CoSay)
return CLASS_E_CLASSNOTAVAILABLE;
CoSayFactory *pCF = new CoSayFactory;
HRESULT hr = pCF->Query!nterface(riid, ppv);
if (FAILED(hr))
delete pCF;
return hr;
}
На этом модификация сервера завершается. Дайте команду Build > Rebuild и устраните ошибки, если они имеются. Затем вновь откройте проект клиентского приложения SayClient и внесите изменения в функцию main, которая теперь должна работать с объектами СОМ более изощренным способом. Она должна сначала загрузить СОМ-сервер и запросить адрес его фабрики классов, затем создать с ее помощью объект CoSay, попросив у него адрес интерфейса isay, и лишь после этого можно начать управление объектом. Последовательность освобождения объектов тоже должна быть тщательно выверена. Ниже приведена новая версия файла SayClient.cpp:
#include "interfaces.h"
void main()
{
(reinitialize (0) ;
IClassFactory *pCF;
// Мы зарегистрировали только один класс CoSay,
// поэтому ищем DLL с его помощью, но при этом
// создается не объект CoSay, а объект CoSayFactory
// (см. код функции DllGetClassObject).
// Поэтому здесь мы просим дать адрес
// интерфейса IClassFactory
HRESULT hr = CoGetClassObject(CLSID_CoSay, CLSCTX_INPROC_SERVER,0, IID_IClassFactory,(void**)&pCF);
if (FAILED(hr))
{
MessageBox(0,"Could not Get Class Factory !
", "CoGetClassObject", MB_OK);
CoUninitialize();
return;
}
// Далее мы с помощью фабрики классов
// создаем объект CoSay и просим его
// дать нам адрес интерфеса ISay
ISay *pSay;
hr = pCF->Create!nstance(0,IID_ISay, (void**)&pSay) ;
if (FAILED(hr))
{
MessageBox(0,"Could not create CoSay and get ISay!
", "Createlnstance", MB_OK);
CoUninitialize ();
return;
}
// Уменьшаем счетчик числа пользователей
// фабрикой классов pCF->Release();
//====== Управляем объектом
pSay->Say();
BSTR word = SysAllocString(L"Yes, My Lord");
pSay->SetWord(word);
SysFreeString(word); pSay->Say();
//====== Уменьшаем число его пользователей
pSay->Release();
SCoUninitialize () ;
}
Запустите приложение (Ctrl+F5) и проверьте его работу. Алгоритм проверки остается тем же, что и ранее, но здесь мы должны по логике разработчиков СОМ, радоваться тому, что выполняем большее число правил и стандартов, а также имеем возможность одновременно создавать несколько СОМ-объектов.
Примечание 1
Примечание 1
На мой взгляд, не может быть ничего лучшего, чем получить код хорошо продуманного класса C++, который дает вам новую, хорошо документированную функциональность. При этом вы получаете полную свободу в том, как ее использовать, и имеете возможность развивать ее по вашему усмотрению. Использование методов класса предполагает выполнение оговоренных заранее правил игры, так же как и при использовании методов интерфейсов. Но эти правила значительно более естественные, чем правила СОМ. Вы, возможно, возразите, что для внедрения в проект нового класса, сам проект надо строить заново. Двоичный объект СОМ в этом смысле внедрить проще. Но здесь надо учитывать тот факт, что для реализации всех выгод СОМ вам придется разработать универсальный контейнер объектов, который будет способен внедрять СОМ-объекты будущих поколений и управлять ими. Это невозможно сделать, не трогая кода вашего приложения. Разработчик более или менее серьезного проекта постоянно корректирует его, изменяя код того или иного модуля. Он просто обречен на это. На мой взгляд, при реализации новых идей проще использовать исходные коды классов, чем двоичные объекты. Без сомнения, за хорошие коды надо платить, также как и за хорошие СОМ-объекты.
Файл описания DLL
Файл описания DLL
Для успешной работы DLL следует добавить к проекту файл ее описания (DEF-файл). Этот способ является альтернативным и, возможно, более простым, чем использование описателей _declspec(dllexport) для экспортируемых функций.
DEF-файл сопровождает DLL и содержит список функций, экспортируемых ею. Создайте новый файл MyCom.def и введите в него такие строки:
LIBRARY "MYCOM.dll"
EXPORTS DllGetClassObject PRIVATE
Заметим, что теперь нет необходимости нумеровать экспортируемые функции, как это делалось ранее (например, в рамках Visual Studio 6). Там вы должны были бы задать:
DllGetClassObject @1 PRIVATE
При наличии DEF-файла компоновщик создает (кроме основного файла библиотеки MyCom.dll) еще два необходимых файла: MyCom.lib (заголовков экспортируемых функций) и МуСот.ехр (информации об экспортируемых функциях и классах). При отсутствии последних двух файлов система не сможет обратиться к функции DllGetClassObject, а следовательно, и к нашему СОМ-объекту CoSay. Для того чтобы DEF-файл участвовал в процессе сборки DLL, в рамках Visual Studio 6 его достаточно было лишь подключить к проекту. Этого шага, однако, недостаточно в рамках Studio.Net. Надо сделать такую установку:
Следующим шагом вы должны зарегистрировать сервер, то есть внести в реестр Windows записи, которые регистрируют факт существования и местоположение DLL. При работе с ATL это действие будет автоматизировано, но сейчас создайте и подключите к проекту еще один файл MyCom.reg, формат которого соответствует командам регистрации, воспринимаемым редактором реестра RegEdit.exe. При этом вам, вероятна, придется действовать альтернативным способом, описанным выше. По крайней мере в бета-версии Studio.Net, с которой я имею дело, в списке типов добавляемых файлов отсутствует тип REG. В текст, приведенный ниже, вы должны подставить идентификаторы, соответствующие вашей регистрации, а также ваш путь к файлу MyCom.dll:
REGEDIT
HKEY_CLASSES_ROOT\MyCom.CoSay\CLSID =
{9B865820-2FFA-lld5-98B4-OOE0293F01B2}
HKEY_CLASSES_ROOT\CLSID\
{9B865820-2FFA-lld5-98B4-OOE0293F01B2}
= MyCom.CoSay
HKEY_CLASSES_ROOT\CLSID\
{9B865820-2FFA-lld5-98B4-OOE0293F01B2}
\InprocServer32 = D:\MyCom\Debug\MyCom.dll
Обратите внимание на то, что текст каждой из трех команд не должен разрываться символами перехода на другую строку. В книге мы вынуждены делать переносы, которых не должно быть в вашем файле. Сохраните и закройте файл. Теперь для регистрации сервера и вложенного в него класса СОМ-объекта надо дважды щелкнуть по имени файла MyCom.reg в окне Windows File Manager или Windows Explorer и согласиться с реакцией системы типа «Вы действительно хотите...» После этого соберите проект, дав команду Build > Build. Процесс сборки должен пройти без ошибок. Теперь наш простейший DLL СОМ-сервер зарегистрирован и готов к использованию.
Сейчас мы займемся разработкой DLL
Сейчас мы займемся разработкой DLL СОМ-сервера, выполняемого в пространстве процесса другого (клиентского) приложения. Для того чтобы понять, что кроется за этой вывеской, мы для начала создадим минимально-простой СОМ-объект и при этом специально не будем пользоваться какими-либо библиотеками или инструментами Studio.Net.
Наш объект будет предоставлять миру только один интерфейс isay, инкапсулирующий два метода: Say и SetWord. Первый метод выводит текстовую строку типа BSTR в окно типа MessageBox, а второй — позволяет изменять эту строку. Тип BSTR в Win32 является адресом двухбайтовой Unicode-строки. Его советуют использовать в СОМ-объектах для обеспечения совместимости с клиентскими приложениями, написанными на других языках.
Я надеюсь, что логика, заложенная в этом простом приложении, поможет вам не терять нить повествования при разработке следующего, более сложного объекта с помощью ATL. Использование ATL и инструментов Studio.Net упрощают разработку СОМ-объектов, но скрывают суть происходящего, вызывая иногда чувство досады и неудовлетворенности. С помощью мастера AppWizard создайте шаблон приложения типа Win32 Dynamic-Link Library (Динамически компонуемая библиотека Win32) под именем МуСот.
Примечание 1
Примечание 1
Это же действие можно выполнить более сложным способом, но зато сход-ным с тем, как это делалось в Visual Studio 6. Дайте команду File > New > File, выберите тип файла и нажмите кнопку Open. Кроме этих действий придется записать новый файл в папку с проектом и подключить его. Для этого используется команда Project > Add Existing Item с последующим поиском файла. Альтернативой этому является перетаскивание существующего файла в окне Solution Explorer из папки Resource Files в папку Header Files.
//=== Эти директивы нужны для того, чтобы не допустить
//=== повторное подключение файла
#if !defined(MY_ISAY_INTERFACE)
#define MY__ISAY_INTERFACE
#pragma once
//====== Для того, чтобы были доступны COM API
#include
//====== Для того, чтобы был виден lUnknown
#include
// Интерфейс ISay мы собираемся зарегистрировать и
// показать миру. Он, как и положено, происходит от
// IUnknown и содержит чисто виртуальные функции
interface ISay : public lUnknown
{
//=== 2 метода, которые интерфейс
//=== предоставляет своим клиентам
virtual HRESULT _stdcall Say 0=0;
virtual HRESULT _stdcall SetWord (BSTR word)=0;
}
#endif
Абстрактный интерфейс не может жить сам по себе. Он должен иметь класс-оболочку (wrapper class), который на деле реализует виртуальные методы Say и SetWord. Этот так называемый ко-класс (класс СОМ-компонента) производится от интерфейса ISay и предоставляет тела всем унаследованным (чисто) виртуальным методам своего родителя. Так как у интерфейса ISay, в свою очередь, имеется родитель (lUnknown), то класс должен также дать реальные тела всем трем методам IUnknown.
Примечание 2
Примечание 2
Если вы хотите, чтобы класс реализовывал несколько интерфейсов, то вы должны использовать множественное наследование. Такой подход проповедует ATL (Active Template Library). MFC реализует другой подход к реализации интерфейсов. Он использует вложенные классы. Каждому интерфейсу соответствует новый класс, вложенный в один общий класс СОМ-объекта.
Для того чтобы быть доступным тем приложениям, которые захотят воспользоваться услугами СОМ-объекта, сам класс тоже должен иметь дом (в виде inproc-сервера DLL). Сейчас, разрабатывая проект типа Win32 DLL, мы строим именно этот дом. С помощью механизма DLL класс будет доступен приложению-клиенту, в адресное пространство процесса которого он загружается. Вы знаете, что DLL загружается в пространство клиента только при необходимости.
Нам неоднократно понадобятся услуги инструмента Studio.Net под именем GuidGen, поэтому целесообразно ввести в меню Tools (Инструментальные средства) Studio.Net новую команду для его запуска. GuidGen, так же как и UuidGen, умеет генерировать уникальные 128-битовые идентификаторы, но при этом он использует удобный Windows-интерфейс. А идентификаторы понадобятся нам для регистрации сервера и класса CoSay. Для введения новой команды:
// {170368DO-85BE-43af-AE71-053F506657A2}
DEFINE_GUID («name»,
0xl70368d0, 0x85be, 0x43af, 0xae, 0x71, 0x5, Ox3f, 0x50,
0x66, 0x57, Oxa2);
Замените аргумент «name» на HD_ISay. Повторите всю процедуру и создайте идентификатор для ко-класса CoSay, который вставьте сразу за идентификатором интерфейса ISay. На сей раз замените аргумент «name» на CLSiD_CoSay, например:
// {9B865820-2FFA-lld5-98B4-OOE0293F01B2}
DEFINE_GUID(CLSID_CoSay,
0х9b865820, 0x2ffa, 0xlldS, 0x98, 0xb4, 0x0, 0xe0, 0x29,
0x3f, 0xl, 0xb2);
Сохраните и закройте файл interfaces.h, так как мы больше не будем вносить в него изменений. Если вы хотите знать, что делает макроподстановка DEFINE_GUID, то за ней стоит такое определение:
#define DEFINE_GUID
(name, 1, wl, w2, \ b1, b2, bЗ, b4, b5, b6, b7, b8) \ EXTERN_C
const GUID name \
= { 1, wl, w2, { b1, b2, bЗ,b4, b5, b6, b7, b8 } }
Оно означает, что макрос создает структуру с именем
Интерфейсы — основа СОМтехнологии
Интерфейсы — основа СОМ-технологии
Разработчики СОМ не интересуются тем, как устроены компоненты внутри, но озабочены тем, как они представлены снаружи. Каждый компонент или объект СОМ рассматривается как набор свойств (данных) и методов (функций). Важно то, как пользователи СОМ-объектов смогут использовать заложенную в них функциональность. Эта функциональность разбивается на группы семантически связанных виртуальных функций, и каждая такая группа называется интерфейсом. Доступ к каждой функции осуществляется с помощью указателя на нее. В сущности, вся СОМ-технология базируется на использовании таблицы указателей на виртуальные функции (vtable).
Примечание 1
Примечание 1
Слово interface (также как и слова object, element) становится перегруженным слишком большим количеством смыслов, поэтому будьте внимательны. Интерфейсы СОМ — это довольно строго определенное понятие, идентичное понятию структуры (частного случая класса) в ООП, но ограниченное соглашениями о принципах его использования.
Каждый СОМ-компонент может предоставлять клиенту несколько интерфейсов, то есть наборов функций. Стандартное определение интерфейса описывает его как объект, имеющий таблицу указателей на виртуальные функции (vtable). В файле заголовков BaseTyps.h, однако, вы можете увидеть макроподстановку #def ine interface struct, которая показывает, как воспринимает это ключевое слово компилятор языка C++. Для него интерфейс — это структура (частный случай класса), но для разработчиков интерфейс отличается от структуры тем, что в структуре они могут инкапсулировать как данные, так и методы, а интерфейс по договоренности (by convention) должен содержать только методы. Заметим, что компилятор C++ не будет возражать, если вы внутри интерфейса все-таки декларируете какие-то данные.
Интерфейсы придумали для предоставления (exhibition) клиентам чистой, голой (одной только) функциональности. Существует договоренность называть все интерфейсы начиная с заглавной буквы «I», например lUnknown, ZPropertyNotifySink и т. д. Каждый интерфейс должен жить вечно и поэтому он именуется уникальным 128-битным идентификатором (globally unique identifier), который в соответствии с конвенцией должен начинаться с префикса IID_. Интерфейсы никогда нельзя изменять, усовершенствовать, так как нарушается обратная совместимость. Вместо этого создают новые вечные интерфейсы.
Примечание 2
Примечание 2
Это непреложное требование справедливо относят к недостаткам СОМ-техно-логии, так как непрерывное усовершенствование компонентов влечет появление слишком большого числа новых интерфейсов, зарегистрированных в вашем реестре. С проблемой предлагают бороться весьма сомнительным образом — тщательным планированием компонентов. Трудно, если вообще возможно, планировать в наше время (тем более рассчитывать на вечную жизнь СОМ-объекта), когда сами информационные технологии появляются и исчезают, как грибы в дождливый сезон.
Классы можно производить от интерфейсов (и наоборот), а каждый интерфейс должен в конечном счете происходить от интерфейса lUnknown. Поэтому все интерфейсы и классы, производные от них, наследуют и реализуют функциональность lUnknown. В связи с такой важностью и популярностью этого интерфейса рассмотрим его поближе. Он определяет общую стратегию использования любого объекта СОМ:
interface lUnknown
{
public: virtual HRESULT _stdcall Querylnterface(REFIID riid,
void **ppvObject) = 0;
virtual ULONG _stdcall AddRef(void) = 0;
virtual ULONG _stdcall Release(void) = 0;
};
Как видите, «неизвестный» содержит три чисто виртуальные функции и ни одного элемента данных. Каждый новый интерфейс, который создает разработчик, должен иметь среди своих предков I Unknown, а следовательно, он наследует все три указанных метода. Первый метод Querylnterface представляет собой фундаментальный механизм, используемый для получения доступа к желаемой функциональности СОМ-объекта. Он позволяет получить указатель на существующий интерфейс или получить отказ, если интерфейс отсутствует. Первый — входной параметр riid — содержит уникальную ссылку на зарегистрированный идентификатор желаемого интерфейса. Это та уникальная, вечная бирка (клеймо), которую конкретный интерфейс должен носить вечно. Второй — выходной параметр — используется для записи по адресу ppvOb j ect адреса запрошенного интерфейса или нуля в случае отказа. Дважды использованное слово адрес оправдывает количество звездочек в типе void**. Тип возвращаемого значения HRESULT, обманчиво относимый к семейству handle (дескриптор), представляет собой 32-битное иоле данных, в котором кодируются признаки, рассмотренные нами в четвергом уроке.
Предположим, вы хотите получить указатель на какой-либо произвольный интерфейс 1Му, уже зарегистрированный системой и получивший уникальный идентификатор IID_IMY, с тем чтобы пользоваться предоставляемыми им методами. Тогда следует действовать по одной из общепринятых схем1:
//====== Указатель на незнакомый объект
lUnknown *pUnk;
// Иногда приходит как параметр IМу *рМу;
// Указатель на желаемый интерфейс
//====== Запрашиваем его у объекта
HRESULT hr=pUnk->Query!nterfасе(IID_IMY,(void **)&pMy);
if (FAILED(hr)) // Макрос, расшифровывающий HRESULT
{
//В случае неудачи
delete pMy; // Освобождаем память
//====== Возвращаем результат с причиной отказа
return hr;
else //В случае успеха
//====== Используем указатель для вызова методов:
pMy->SomeMethod();
pMy->Release(); // Освобождаем интерфейс
Возможна и другая тактика:
//====== В случае успеха (определяется макросом)
if (SUCCEEDED(hr))
{
//====== Используем указатель
}
else
{
//====== Сообщаем о неудаче
}
Второй параметр функции Queryinterf асе (указатель на указатель) позволяет возвратить в вызывающую функцию адрес запрашиваемого интерфейса. Примерная схема реализации метода Queryinterf асе (в классе СОМ-объекта, производном от IМу) может иметь такой вид:
HRESULT _stdcall СМу::Queryinterfасе(REFIID id, void **ppv)
{
//=== В *ppv надо записать адрес искомого интерфейса
//=== Пессимистический прогноз (интерфейс не найден)
*ppv = 0;
// Допрашиваем REFIID искомого интерфейса. Если он
// нам не знаком, то вернем отказ E_NOINTERFACE
// Если нас не знают, но хотят познакомиться,
// то возвращаем свой адрес, однако приведенный
// к типу "неизвестного" родителя
if (id == IID_IUnknown)
*ppv = static_cast
// Если знают, то возвращаем свой адрес приведенный
// к типу "известного" родителя IМу
else if (id == IID_IMy)
*ppv = static_cast
//===== Иначе возвращаем отказ else
return E_NOINTERFACE;
//=== Если вопрос был корректным, то добавляем единицу
//=== к счетчику наших пользователей
AddRef();
return S_OK;
}
Методы AddRef и Release управляют временем жизни объектов посредством подсчета ссылок (references) на пользователей интерфейса. В соответствии с общей концепцией объект (или его интерфейс) не может быть выгружен системой из памяти, пока не равен нулю счетчик ссылок на его пользователей. При создании интерфейса в счетчик автоматически заносится единица. Каждое обращение к AddRef увеличивает счетчик на единицу, а каждое обращение к Release — уменьшает. При обнулении счетчика объект уничтожает себя сам. Например, так:
ULONG СМу::Release()
{
//====== Если есть пользователи интерфейса
if (—m_Ref != 0)
return m_Ref; // Возвращаем их число
delete this;
// Если нет — уходим из жизни,
// освобождая память
return 0;
}
Вы, наверное, заметили, что появилась переменная m_Ref. Ранее было сказано об отсутствии переменных у интерфейсов. Интерфейсы — это голая функциональность. Но обратите внимание на тот факт, что метод Release принадлежит не интерфейсу 1Му, а классу ему, в котором переменные естественны. Обычно в классе СОМ-объекта и реализуются чисто виртуальные методы всех интерфейсов, в том числе и главного интерфейса zunknown. Класс ему обычно создает разработчик СОМ-объекта и производит его от желаемого интерфейса, например, так:
class СМу : public IMy
{
// Данные и методы класса,
// в том числе и методы lUnknown
};
В свою очередь, интерфейс IMy должен иметь какого-то родителя, может быть, только iUnknown, а может быть одного из его потомков, например:
interface IMy : IClassFactory
{
// Методы интерфейса
};
СОМ-объектом считается любой объект, поддерживающий хотя бы lUnknown. Историческое развитие С ОМ-технологий определило многообразие терминов типа: OLE 94, OLE-2, OCX-96, OLE Automation и т. д. Элементы ActiveX принадлежат к той же группе СОМ-объектов. Каждый новый термин из этой серии подразумевает все более высокий уровень предоставляемых интерфейсов. Элементы ActiveX должны как минимум обладать способностью к активизации на месте, поддерживать OLE Automation, допуская чтение и запись своих свойств, а также вызов своих методов.
Использование макросов COM
Использование макросов COM
Разработчики COM рекомендуют для повышения надежности и переносимости компонентов использовать при их разработке множество макроопределений, которые вы также вынуждены будете использовать при разработке проекта на базе ATL. Например, макрос STDMETHODIMP при раскрытии заменяет спецификаторы HRESULT _stdcall. Для того чтобы приобрести навыки использования макросов СОМ, мы применим их в файлах MyCom.h и MyCom.cpp. Сравнивая старую и новую версии этих файлов, вы без труда поймете смысл макроподстановок. В файл MyCom.h ведите коррекцию кодов так, как показано ниже:
#if !defined(MY_COSAY_HEADER)
#define MY_COSAY_HEADER
#pragma once
#include "MyComTLib_h.h" class CoSay : public ISay
//====== Класс, реализующий интерфейсы ISay, lUnknown
public: CoSay (') ;
virtual -CoSay();
// lUnknown
STDMETHODIMP QuerylnterfacefREFIID riid, void** ppv);
STDMETHODIMP_(ULONG) AddRef();
STDMETHODIMP_(ULONG) Release();
// ISay
STDMETHODIMP Say();
STDMETHODIMP SetWord (BSTR word);
private:
//====== Счетчик числа пользователей классом
ULONG m_ref;
//====== Текст, выводимый в окно
BSTR m_word;
};
//====== Фабрика классов СОМ DLL-сервера
class CoSayFactory : public IClassFactory
{
public:
CoSayFactory();
virtual ~CoSayFactory();
// lUnknown
STDMETHODIMP QueryInterface(REFIID riid, void** ppv) ;
STDMETHODIMP_(ULONG) AddRef();
STDMETHODIMP_(ULONG) Release();
// IClassFactory
STDMETHODIMP Createlnstance(LPUNKNOWN pUnk,
REFIID riid, void** ppv);
STDMETHODIMP LockServer(BOOL bLock);
private:
ULONG m_ref; };
#endif
Теперь перейдите к файлу MyCom.cpp и произведите замены в соответствии с текстом, приведенным ниже:
#include "MyComTLib_i.c"
#include "MyCom.h"
//====== Произвольный ограничитель длины строк
#define MAX_LENGTH 128
//====== Счетчик числа блокировок DLL
ULONG gLockCount;
//====== Счетчик числа пользователей СОМ-объектами
ULONG gObjCount;
CoSay::CoSay()
{
//=== Обнуляем счетчик числа пользователей класса,
//=== так как интерфейс пока не используется
m_ref = 0;
//=== Динамически создаем строку текста по умолчанию
m_word = SysAllocString(L"This is MyComTLib speaking");
gObjCount++;
}
CoSay::-CoSay()
{
//====== при завершении работы освобождаем память
if (m_word)
SysFreeString(m_word);
gObjCount—;
}
//====== Реализация методов lUnknown
STDMETHODIMP CoSay::QueryInterface(REFIID riid, void** ppv)
{
// Стандартная логика работы с клиентом
// Поддерживаем только два интерфейса
//====== Реализация lUnknown *ppv = 0;
if (riid==IID_IUnknown)
*ppv = static_cast
else if (riid==IID_ISay)
*ppv = static_cast
else
return E_NOINTERFACE;
//====== Добавляем единицу к счетчику
//====== пользователей нашим объектом
AddRef () ;
return S_OK;
}
STDMETHODIMP_(ULONG) CoSay::AddRef()
{
return ++m_ref;
}
STDMETHODIMP_(ULONG) CoSay: :Release ()
{
if (--m_ref==0)
delete this;
return m_ref;
}
//====== Реализация ISay
STDMETHODIMP CoSay::Say()
{
//=== Преобразование типов (из BSTR в char*),
//=== которое необходимо для использования
MessageBox char buff[MAX_LENGTH];
WideCharToMultiByte(CP_ACP, 0, m_word, -1,
buff, MAX_LENGTH, 0, 0);
MessageBox (0, buff, "Interface ISay:", MB_OK);
return S_OK;
}
STDMETHODIMP CoSay::SetWord(BSTR word)
{
//====== Повторное зыделение памяти
SysReAllocString(&m_word, word);
return S_OK;
}
STDAPI DllGetClassObject (REFCLSID rclsid,
REFIID riid, LPVOID* ppv)
{
if (rclsid != CLSID_CoSay)
return CLASS_E_CLASSNOTAVAILABLE;
CoSayFactory *pCF = new CoSayFactory;
HRESULT hr = pCF->Query!nterface(riid, ppv);
if (FAILED(hr)) delete pCF;
return hr;
}
STDAPI DllCanUnloadNow()
{
//====== Если счетчики нулевые, то мы позволяем
//====== системе выгрузку DLL-сервера
return IgLockCount && IgObjCount ? S_OK : S_FALSE;
}
//====== Фабрика классов
CoSayFactory::CoSayFactory()
{
m_ref = 0;
gObjCount++;
}
CoSayFactory::-CoSayFactory()
gObjCount--;
}
//====== Методы lUnknown
STDMETHODIMP CoSayFactory
::QueryInterface(REFIID riid, void** ppv)
{
*ppv = 0;
//=== Обходимся без шаблона static casto
if (riid == IID_IUnknown)
*ppv = (lUnknown*)this;
else if (riid == IID_IClassFactory)
*ppv = (IClassFactory*)this;
else
return E_NOINTERFACE;
AddRef () ;
return S_OK;
}
STDMETHODIMP_(ULONG) CoSayFactory::AddRef()
{
return ++m_ref;
}
STDMETHODIMP_(ULONG) CoSayFactory::Release()
{
if (--m_ref==0)
delete this;
return m_ref;
}
//====== Методы IClassFactory
STDMETHODIMP CoSayFactory::CreateInstance(LPUNKNOWN pUnk,
REFIID riid, void** ppv)
{
// Этот параметр управляет аггрегированием объектов СОМ,
// которое мы не поддерживаем if (pUnk)
return CLASS_E_NOAGGREGATION;
//=== Создание нового объекта и запрос его интерфейса
CoSay *pSay = new CoSay;
HRESULT hr = pSay->Query!nterface (riid, ppv);
if (FAILED(hr))
delete pSay;
return hr;
}
//=== Управление счетчиком фиксаций сервера в памяти
STDMETHODIMP CoSayFactory::LockServer(BOOL bLock)
{
if (bLock) // Если TRUE, то увеличиваем счетчик
++gLockCount;
else // Иначе — уменьшаем
--gLockCount; return S_OK;
}
Библиотеку типов также надо регистрировать для того, чтобы клиент мог найти ее с помощью уникального идентификатора. Введите изменения в файл MyCom.reg в соответствии со схемой, приведенной ниже, но используя при этом ваши идеитификаторы, файловые адреса и помня о правилах переноса. Сохраните исправления и зарегистрируйте все перечисленные объекты, дважды щелкнув на файле MyCom.reg в окне Windows File Manager:
REGEDIT HKEY_CLASSES_ROOT\MyComTLib.CoSay\CLSID =
{9B865820-2FFA-lld5-98B4-OOE0293F01B2}
HKEY_CLASSES_ROOT\CLSID\
{9B865820-2FFA-lld5-98B4-OOE0293F01B2}
= MyComTLib.CoSay
HKEY_CLASSES_ROOT\CLSID\
{9B865820-2FFA-lld5-98B4-OOE0293F01B2}
\InprocServer32 =
D:\My Projects\MyComTLib\Debug\MyComTLib.dll'
HKEY_CLASSES_ROOT\CLSID\
{9B865820-2FFA-lld5-98B4-OOE0293F01B2}\TypeLib =
{0934DA90-608D-4107-9ECC-C7E828AD0928}
HKEY_CLASSES_ROOT\TypeLib\
{0934DA90-608D-4107-9ECC-C7E828AD0928}
= MyComTLib
HKEY_CLASSES_ROOT\TypeLib\
{0934DA90-608D-4107-9ECC-C7E828AD0928}
\1.0\0\Win32 =
D:\My Projects\MyComTLib\Debug\MyComTLib.tlb
После этого дайте команду Build > Rebuild Solution. При осуществлении компоновки (Linking) в окне Output должна появиться строка:
Creating library Debug/MyComTLib.lib
and object Debug/MyComTLib.exp
которая свидетельствует о том, что DEF-файл воспринят и участвует в построении проекта. Если вы не видите этой строки, то выполните шаги по настройке проекта, которые описаны выше в разделе «Файл описания DLL», и повторите процедуру построения. После этого сервер готов к использованию.
Как работает DLL
Как работает DLL
Вы уже знаете, что созданный и подключенный компоновщиком динамический модуль система интегрирует в пространство другого (клиентского) процесса, загрузив его по определенному базовому адресу. Любая динамически загружаемая библиотека экспортирует функции, которые пишутся в расчете на то, что их будет вызывать клиентское приложение или другая DLL. Глобальная функция DllMain представляет собой точку входа в динамически подключаемую библиотеку. Она является некоторого рода заглушкой (placeholder) для реального, определяемого библиотекой имени функции. Первый параметр DllMain подан операционной системой и представляет собой Windows-описатель DLL. Его можно использовать при вызове функций, требующих этот описатель, например при вызове GetModuleFileName. Второй параметр указывает причину вызова DLL. Он может принимать одно из четырех значений:
Если DllMain вернет FALSE или 0, то клиентское приложение завершится с кодом ошибки. Характерно, что стратегия работы с СОМ-объектами сходна со стратегией, используемой при работе с DLL. Последняя заключается в том, что каждый вызов функции LoadLibrary увеличивает на единицу счетчик числа пользователей библиотеки. Вызов функции FreeLibrary уменьшает значение счетчика. Обнаружив, что счетчик числа пользователей равен нулю, операционная система автоматически выгрузит ее. Если после этого вызвать какую-либо экспортируемую DLL функцию, то возникнет исключительная ситуация Access Violation, так как код по указанному адресу уже не отображается на адресное пространство процесса.
Возвращаясь к коду, созданному мастером ATL Project wizard, отметим, что кроме DllMain, модуль экспортирует еще 4 функции: DllRegisterServer, DllUnregisterServer, DllCanUnloadNow, DllGetClassObject. Полезно открыть, с помощью окна Solution Explorer файл ATLGL.def, который создал и поместил в папку проекта мастер. Этот файл используется компоновщиком при создании lib-файлов и ехр-файлов, содержащих информацию о DLL и экспортируемых ею функциях. Все эти функции имеют тип STDAPI. На самом деле STDAPI — это макроподстановка, заданная в файле заголовков WinNT.h. С помощью этого файла вы можете самостоятельно расшифровать макрос STDAPI. Он разворачивается (expanded) в такой комплексный описатель:
extern "С" HRESULT _stdcall
Описатель extern «С» означает, что при вызове функция будет использовать имя в стиле языка С, а не C++, то есть описатель отменяет декорацию имен, производимую компилятором C++ по умолчанию.
Примечание 1
Примечание 1
Компилятор C++ использует специальную декорацию имен, для того чтобы отличать overloaded-функции, имеющие одинаковые имена, но разные прото-. типы. Например, вызов: int func(int a, double b); в результате декорации становится: _func@12. Число 12 описывает количество байт, занимаемых списком аргументов. Такая условность называется naming convention (соглашение об именах). Есть и другая конвенция — calling convention (соглашение о связях), которая определяет договоренность о передаче параметров при вызове Win32 API-функций. Описатель _stdcall относится к этой группе. Он определяет: порядок передачи аргументов (справа налево): то, что аргументы передаются по значению (by value), что вызываемая функция должна сама выбирать аргументы из стека и что трансляция регистра символов, верхнего или нижнего, не производится.
Функция DllCanUnloadNow определяет, используется ли данная DLL в данный момент. Если нет, то вызывающий процесс может безопасно выгрузить ее из памяти. Функция DllGetClassObject с помощью третьего параметра (LPVOID* ppv) возвращает адрес так называемой фабрики классов, которая умеет создавать СОМ-объекты, по известному CLSID — уникальному идентификатору объекта.
Откройте файл ATLGLJ.c и.убедитесь, что он пуст. Этот файл будет заполнен кодами компилятором MIDL, о котором мы уже говорили ранее. Запустите приложение (Ctrl+F5). Компилятор и компоновщик создадут исполняемый модуль типа DLL, но загрузчик не будет знать в рамках какого процесса (контейнера) следует запустить его на отладку.
Примечание 2
Примечание 2
В этот момент Studio.Net запросит имя ехе-файла, то есть модуля или процесса в пространство которого должна быть загружена созданная компоновщиком DLL. Вы можете воспользоваться выпадающим списком для выбора строки Browse, которая даст диалог по выбору файла. Найдите с его помощью стандартный контейнер для отладки элементов ActiveX (tstcon32.exe), поставляемый вместе со Studio.Net по адресу:...\MicrosoftVisualStudio.Net\Common7\Tools и нажмите Open, а затем ОК.
В рамках тестового контейнера можно отлаживать работу элементов ActiveX, OLE-controls и других СОМ-объектов. Но сейчас наш объект еще не готов к этому, так как мы не создали СОМ-класса, инкапсулирующего желаемые интерфейсы. Поэтому закройте тестовый контейнер, вновь откройте в рамках Studio.Net уже существующий IDL-файл (Interface Description Language file) ATLGLidl и просмотрите коды, описывающие интерфейс, СОМ-класс и библиотеку типов. Вы помните, что этот файл обрабатывается компилятором MIDL, который на его основе создает другие файлы. Откройте файл ATLGM.c и убедитесь, что теперь он не пуст. Его содержимое было создано компилятором MIDL. В данный момент файл ATLGM.c содержит только один идентификатор библиотеки, который регистрируется с помощью макроподстановки MIDL_DEFINE_GUID.
Как работают СОМсерверы
Как работают СОМ-серверы
Созданный и подключенный компоновщиком динамически загружаемый модуль сервера система интегрирует в пространство другого (клиентского) процесса, загрузив его по определенному базовому адресу. Любая динамически загружаемая библиотека экспортирует функции, которые пишутся в расчете на то, что их будет вызывать клиентское приложение или другая DLL. Как только DLL спроецирована на адресное пространство вызывающего процесса, ее данные и функции становятся доступными клиенту и представляют собой просто дополнительный код и данные, как-то оказавшиеся в адресном пространстве процесса.
СОМ-серверы, которые хранятся в DLL-файлах, называются внутризадачными (in-process) серверами. Но они могут быть реализованы и в виде ЕХЕ-файлов. Тогда они называются либо локальными (local) серверами, либо удаленными (remote) серверами. Приложение-клиент и локальный сервер функционируют в отдельных процессах или адресных пространствах в рамках одной машины. Клиент и удаленный сервер функционируют не только в отдельных процессах (адресных пространствах), но и разделены сетевыми каналами связи. И тем и другим необходим коммуникационный мост, чтобы вызывать функции и передавать друг другу данные. Такой мост обеспечивают библиотеки OLE, которые в качестве средства реализации используют механизм RFC (Remote Procedure Call — удаленный вызов процедуры). , Существует еще одна классификация СОМ или OLE-объектов. В рамках MFC и поддерживаемой ею архитектуры документ — представление мы можем создать объекты, которые либо поддерживают связь (linked) с приложением-контейнером, либо внедрены в него (embedded). Некоторые приложения поддерживают как связывание, так и внедрение объектов. Основное различие между двумя типами OLE-объектов заключается в том, что источник данных внедренного (embedded) объекта является частью документа контейнера и хранится вместе с данными контейнера, в то время как данные связанного (linked) объекта хранятся в документе сервера, то есть в файле, созданном и управляемым сервером. Объект контейнера, который связан (linked), хранит лишь информацию, необходимую для связи с документом сервера. Говорят, что объект контейнера хранит связь с документом сервера. Приложение-сервер, поддерживающее связывание, должно уметь копировать свои данные в буфер обмена для выполнения нужд контейнера по копированию объекта. Обычно под внедренным объектом понимается обобщенный объект, независимо от способа общения с ним (linked или embedded).
В конце этого урока мы (в рамках другой библиотеки — ATL) создадим DLL-сервер, который выполняет роль простейшего элемента ActiveX, внедряемого в окно приложения-клиента. Но сначала подробно рассмотрим, как взаимодействуют клиент и сервер в рамках приложения, использующего «сырые» (raw) функции COM API, с разработки которых и началось движение СОМ.
Концепция маршалинга
Концепция маршалинга
СОМ спроектирован так, чтобы обеспечить прозрачную (transparent) коммуникацию клиента с сервером независимо от того, где они находятся:
С точки зрения клиента все СОМ-объекты управляются одинаковым способом — с помощью указателя на интерфейс, который должен действовать в адресном пространстве клиента. Если СОМ-объект находится в этом же пространстве, то вызов метода какого-либо из его интерфейсов осуществляется прямо, без посредников. Если объект расположен вне рамок клиентского процесса, то вызов осуществляется с помощью посредников, называемых заглушками. Их либо автоматически генерирует СОМ, либо создает сам разработчик.
С точки зрения сервера все вызовы также осуществляются с помощью указателя на интерфейс. Но теперь указатель должен действовать в контексте процесса серверного приложения. Если процессы совпадают (inproc-server), то можно обойтись без заглушек, но если нет, то нужен еще один посредник, который расположен в пространстве серверного процесса.
Для того чтобы клиент, написанный на любом из перечисленных (элитных) языков, мог вызвать метод интерфейса из СОМ-объекта, расположенного в рамках другого процесса, несколько компонентов должны объединить свои усилия. Прежде всего это две заглушки (клиентская и серверная). В технологии RPC (Remote Procedure Call) они так и называются. В СОМ клиентская заглушка называется proxy stub, или просто proxy (представитель интересов сервера).
Когда клиент вызывает метод локального или удаленного сервера (Рисунок 8.1), этот вызов перехватывается представителем настоящего сервера, расположенным в адресном пространстве клиента (proxy). Последний получает запрос на вызов метода, упаковывает параметры, которые будут посланы серверу, и вызывает соответствующий метод при помощи RPC. Акт передачи данных, то есть параметров функций и возвращаемых значений, за пределы процесса называется транспортировкой. Она включает в себя упаковку, передачу и распаковку данных по достижении ими места назначения. Отметьте, что транспортировать надо как данные, так и интерфейсные указатели.
С другой стороны, специальная часть кода на сервере (stub), получает от proxy запрос на вызов метода, распаковывает параметры и вызывает нужный метод реального сервера. Сервер, выполнив клиентский запрос, обычно возвращает какие-то данные. Посредник на стороне сервера (stub) перехватывает эти данные, упаковывает их и направляет соответствующему посреднику на стороне клиента (proxy). Последний получает возвращаемые данные, распаковывает их и передает клиенту. Библиотеки СОМ автоматически обеспечивают код функций proxy/ stub для стандартных интерфейсов. При написании же собственных интерфейсов следует пользоваться интерфейсом, производным от iMarshal. Итак, заместитель расположен в адресном пространстве клиента и представляет интересы СОМ-объекта на стороне клиента, обеспечивая суррогатные точки входа для каждого из методов, обозначенных в исходном IDL-файле. Когда клиент делает вызов удаленной (remote) процедуры сервера, то сначала он вызывает суррогат этой процедуры в заглушке proxy (в пространстве своего процесса). Последняя осуществляет:
Серверная заглушка, или просто stub, распаковывает (unmarshals) параметры и передает их объекту СОМ. Она также запаковывает ответную информацию, возвращаемые параметры, для того чтобы передать их назад клиенту.
Описанные действия называются маршализацией аргументов. Эта процедура сильно зависит от типа параметров. Например, маршализация массива данных значительно сложнее маршализации переменной целого типа или указателя на структуру. Для каждого типа данных существуют свои отдельные функции. Proxy состоит из части, которая размещена в OLE32. DLL (proxy manager), и частей, которые зависят от интерфейсов СОМ-объекта (interface proxies). Для клиента proxy представляет собой реальный СОМ-объект.
Сам канал передачи обслуживается функциями библиотеки СОМ. Канал передает буфер (с маршализованными параметрами) во владение функциям из RPC-библиотеки, которые и занимаются его передачей через границу между процессами. Вы можете выбирать между стандартной маршализацией, обеспечиваемой библиотекой СОМ, и своей собственной (custom marshaling). В последнем случае вы должны разработать интерфейс, производный от IMarshal. Каждый отдельный интерфейс может пользоваться одним из двух способов маршализации своих параметров. Это определяется на этапе проектирования СОМ-класса, реализующего интерфейсы. Здесь уместно привести схему, которую вы также можете увидеть в MSDN (Search > Marshaling Details).
Модель программирования COM
Модель программирования COM
Любой программный продукт представляет собой набор данных и функций, которые как-то используют, обрабатывают эти данные. Этот принцип, как вы знаете, лежит в основе ООП. Там класс инкапсулирует данные и методы, которые служат для управления ими. Сходный принцип используется и в модели программирования СОМ. СОМ-объектом (или OLE-объектом) называется такой программный продукт, который обеспечивает доступ к данным с помощью одного или нескольких наборов функций, которые называются интерфейсами.
В отличие от ООП, которое рассматривает интеграцию классов на уровне исходных модулей — текстов программ, СОМ рассматривает интеграцию компонентов на двоичном уровне, то есть на уровне исполняемых модулей. Цель — многократное использование удачно разработанных компонентов именно на этом уровне. Двоичный уровень дает независимость от аппаратной архитектуры и языков программирования (взамен порождая массу других проблем). Двоичный стандарт взаимодействия позволяет СОМ-объектам, разработанным разными поставщиками и на разных языках, эффективно взаимодействовать друг с другом. С практической точки зрения СОМ — это набор системных библиотек (DLL-файлов), которые дают возможность разным приложениям, выполненных с учетом требований СОМ, взаимодействовать друг с другом. Исторически сложилось так, что СОМ состоит из нескольких различных технологий, которые пользуются услугами друг друга для формирования объектно-ориентированной системы. Каждая технология реализует определенный набор функций.
Преимуществами двоичных компонентов являются: взаимозаменяемость, возможность многократного использования, возможность параллельной разработки с последующей сборкой в одном проекте. Недостатки СОМ настолько очевидны, что я не буду их перечислять. Вы почувствуете их в тот момент, когда начнете самостоятельно разрабатывать свой первый СОМ-объект. Приведем далеко не полный список литературы, который поможет более детально разобраться в технологии СОМ.
СОМ реализует модель «клиент-сервер». Объекты, называемые серверами, предоставляют набор функций в распоряжение других объектов, называемых клиентами, но СОМ-объект может быть одновременно и клиентом, и сервером. Серверы всегда подчиняются спецификациям СОМ, в то время как клиенты могут быть как СОМ-объектами, так и не быть таковыми. Поставщик СОМ-объектов (сервер) делает объекты доступными, реализуя один или множество интерфейсов. Пользователь СОМ-объектом (клиент) получает доступ к объекту с помощью указателя на один или множество интерфейсов. С помощью указателя клиент может пользоваться объектом, не зная даже как он реализован и где он находится, но быть при этом уверенным, что объект всегда будет вести себя одинаково. В этом смысле интерфейс объекта представляет собой некий контракт, обещающий клиенту надежное поведение, несмотря на язык и местоположение клиента. Благодаря этому решается проблема бесконечных обновлений версий сервера. Новая версия сервера просто добавляет новые интерфейсы, но никогда не изменяет старых. Клиент может либо пользоваться новым интерфейсом, если он о нем знает, либо не пользоваться им, а вместо этого пользоваться старым. Добавление новых интерфейсов никак не влияет на работу клиентов, работающих со старыми. Кроме того, как нас уверяет документация, двоичный уровень делает компоненты независимыми от платформы клиента.
Независимость от языка
Независимость от языка
Разработанный DLL СОМ-сервер выполняет свою функцию, обслуживая клиентское приложение, разработанное на языке C++. Но он не будет работать с приложениями, написанными на других языках. В MS-документации под другими языками имеют в виду СОМ-совместимые языки: VB, VBScript, Visual J++ и С в версии Microsoft. Остальные платформы и языки пренебрегают технологией СОМ и поэтому как бы не существуют.
Так вот, чтобы сделать наш объект доступным из клиентского приложения, разработанного на одном из перечисленных четырех языков, надо познакомиться с еще одним внушительным пластом технологии СОМ. Это язык MIDL (Microsoft Interface Definition Language) и компилятор этого языка (MIDL compiler), который тоже иногда называют просто MIDL. Язык MIDL имеет достаточно много новых для C++ ключевых слов, которые более точно описывают атрибуты интерфейсов, классов и их методов, но он не имеет никаких исполняемых операторов (типа for, if и т. д.). Предположим, что вы создали файл MyCom.idl, в котором более точно описали интерфейсы, класс объекта СОМ и библиотеку его типов. В результате компиляции вашего IDL-файла будут сгенерированы несколько других файлов. В их число входят две заглушки MyCom_i.c и МуСот_р.с на языке С и файл заголовков MyCom.h. Эти файлы теперь можно использовать для обеспечения интерфейса между клиентским и серверным приложениями.
Все начиналось с языка С, но потом было решено, что другие языки тоже должны участвовать в движении СОМ. Проблема совместимости языков возникает потому, что типы данных, используемые в языке С, не совпадают с типами в других языках. Более того, в некоторых из этих языков переменная может по прихоти разработчика изменять свой тип по ходу программы, что совершенно неприемлемо в логике С и C++. В связи с этим и был разработан метаязык более высокого уровня, который используется только для определений (definitions) всех данных, связанных с объектами СОМ, и сопряжения их типов. MIDL пришел на смену языку ODL (Object Description Language) и его компилятору MkTypeLib. Кроме тогЪ, вы можете встретить упоминания о стандарте DCE RFC IDL (Distributed Computing Environment Remote Procedure Call Interface Definition Language), который тоже устарел, так как не поддерживает определений, связанных с объектами.
При использовании технологий Microsoft вы всегда должны быть готовыми к тому, что для обозначения тех же самых или слегка модифицированных понятий изобретаются абсолютно новые термины, носящие, на мой взгляд, более рекламный, чем смысловой характер. Делая заплату на какие-то явные (или не очень) промахи, целесообразно представить ее в виде новой, даже революционной, технологии, так как этот факт повышает marketability (конкурентоспособность). Но для разработчика это означает лишь дополнительные усилия на выделение истинной сути новшеств и поиск тождественных или сходных понятий, без которых трудно выстроить более или менее стройную модель или структуру, призванную помогать в разработке приложений.
Новый проект
Новый проект
Для ознакомления с возможностями MIDL создайте новый пустой проект типа Win32 DLL. Для этого:
//====== Импорт библиотечных определений
import "oaidl.idl";
import "ocidl.idl";
//====== Уточненное описание интерфейса ISay
[
object, uuid(170368DO-85BE-43af-AE71-053F506657A2) ,
helpstring("My Test DLL COM-server ISay")
]
interface ISay : lUnknown
{
HRESULT Say();
HRESULT SetWord([in]BSTR word);
}
//====== Описание библиотеки типов
[
uuid(0934DA90-608D-4107-9ECC-C7E828AD0928),
version(1.0),
helpstring("My Test DLL COM-server Type Library")
]
library MyCom {
importlib("stdole32.tlb") ;
[uuid(9B865820-2FFA-lld5-98B4-OOE0293F01B2)]
//====== Описание класса реализации интерфейса
coclass CoSay
{
[default] interface ISay; };
};
Попробуйте откомпилировать новый файл описания интерфейса, используя клавиатурную комбинацию Ctrl+F7. Если на этом этапе возникнут ошибки, то проверьте настройку проекта View > Property Pages > MIDL > General > MkTy ре Lib Compatible (она должна быть в состоянии No) и повторите компиляцию. После успешного ее завершения просмотрите содержимое папки проекта. В ней должны появиться новые файлы: MyComTLib_h.h, MyComTLibJ.c, MyComTLib_p.c и dlldata.c. Эти файлы, как было сказано, помогают обеспечить взаимодействие клиента с сервером. В результате их компиляции и сборки будет сгенерирована DLL, в которой реализованы коды заглушек proxy/stub.
Для того чтобы двинуться дальше, вам необходимо взять некоторые файлы из папки МуСот с предыдущим проектом типа DLL.
MyComTLib.def : Declares the module parameters. LIBRARY "MYCOMTLIB.dll"
EXPORTS .
DllGetclassObject PRIVATE
DllCanUnloadNow PRIVATE
От сырых COM API к проекту ATL
От сырых COM API к проекту ATL
В этом уроке мы научимся разрабатывать приложения, которые реализуют функции СОМ-сервера и СОМ-контейнера. Известная вам технология OLE (Object Linking and Embedding) базируется на модели COM (Component Object Model), которая определяет и реализует механизм, позволяющий отдельным компонентам (приложениям, объектам данных, элементам управления, сервисам) взаимодействовать между собой по строго определенному стандарту. Технология разработки таких приложений кажется довольно сложной для тех, кто сталкивается с ней впервые. Трудности могут остаться надолго, если не уделить достаточно времени самым общим вопросам, то есть восприятию концепции СОМ (Модель многокомпонентных объектов). Поэтому не жалейте времени и пройдите через все, даже кажущиеся примитивными, этапы развития СОМ-приложений, как серверов, так и контейнеров. Мы начнем с того, что создадим СОМ-сервер с помощью сырых (raw) COM API-функций для того, чтобы лучше понять механизмы взаимодействия компонентов. Эти механизмы будут частично скрыты в следующих приложениях, которые мы будем развивать на основе стартовых заготовок, созданных мастером Studio.Net в рамках возможностей библиотеки шаблонов ATL (Active Template Library).
Проект на основе ATL
Проект на основе ATL
Библиотеки шаблонов, такие как ATL (Active Template Library), отличаются от обычных библиотек классов C++ тем, что они представляют собой множество шаблонов (templates), которые могут и не иметь иерархической структуры. При использовании обычной библиотеки мы создаем класс, производный от какого-то класса из библиотеки и тем самым наследуем всю его функциональность, а значит, и функциональность его предков. С библиотекой шаблонов поступают по-другому. Выбрав шаблон, обращаются к нему для создания нового, класса, .скроенного по образу и подобию шаблона, получая тем самым его общую функциональность. Специфика определяется путем реализации некоторых методов шаблона. Новый класс кроится по шаблону, настраиваемому параметром, который передается в угловых скобках шаблона.
Использование библиотеки ATL полностью снимает с вас заботу о реализации методов ILJnknown, о получении уникальных идентификаторов и регистрации их в системе, а также многие другие рутинные проблемы, связанные с поддержкой технологии СОМ. Вы теперь сможете оценить эти преимущества, так как попробовали создать СОМ-объект с помощью сырых (raw) COM API. У нас нет времени более подробно заниматься технологией СОМ, так как общая направленность книги — использование передовых технологий, а не детальное их изучение. Для получения фундаментальных знаний о технологии мы отсылаем читателя к книгам, перечисленным ранее. Отметим, что текст книги Inside OLE целиком (1200 страниц) помещен в MSDN (см. раздел Books).
Далее рассмотрим, как создать СОМ-объект, обладающий возможностями DLL-сервера (inproc server), Мы создадим новый проект, а в нем остов СОМ DLL-сервера и добавим необходимый нам код, учитывающий специфику СОМ-объекта.
Итак, СОМ DLL-сервер или дом для ко-классов готов. Теперь можно начать процесс начинки его классами (или одним классом), которые, в свою очередь, будут являться домами для экспонируемых интерфейсов. Говорят, что ко-класс реализовывает или экспонирует интерфейсы (или один интерфейс). Просмотрите результаты работы мастера. В файле ATLGL.cpp, здесь уже нарушена традиция MFC разделять объявление и реализацию класса, объявлен класс CATLGLModule, скроенный по шаблону и одновременно производный от класса CAtlDllModuleT. К сожалению, документация по ATL содержит весьма краткие сведения о своих классах. Из нее мы можем, однако, узнать, что шаблон классов CAtlDllModuleT поддерживает функциональность DLL-модуля, который умеет регистрировать себя в качестве такового. Он происходит от класса CAtiModule, у которого есть симметричный потомок CAtlExeModuleT, поддерживающий функциональность ЕХЕ-модуля приложения, и умеет обрабатывать параметры командной строки. Иначе такой модуль называется out-of-proc-сервером (локальным или удаленным сервером). Он выполняется в пространстве собственного процесса, а не клиентского, как в случае in-proc-сервера.
Аналогично MFC-проекту, в котором есть объект theApp, здесь объявлен глобальный объект _AtlModule класса CATLGLModule, унаследованные методы которого позволяют зарегистрировать (DllRegisterServer) в системном реестре наличие нового сервера COM DLL. Но это только начало. Немного позже мы создадим и зарегистрируем СОМ-объект, все его интерфейсы и библиотеку (typelib) упреждающего описания новых объектов COM (coclass, interface, dispinterface, module, typedef). Да, каждый СОМ-объект вносит довольно много записей в системный реестр, поэтому так важно правильно производить обратную процедуру (DllUnregisterServer), иначе реестр превращается в кладбище записей, внесенных объектами, которые уже не существуют в операционной системе.
Разработка клиента
Разработка клиента
с использованием специальных указателей
Создайте новый пустой проект консольного приложения с именем SayTLibClient и вставьте в него новый файл SayTLibClient.cpp. Введите в файл следующий текст и проследите за тем, чтобы текст директивы #import либо не разрывался переносом ее продолжения на другую строку, либо разрывался по правилам, то есть с использованием символа переноса ' \ ', как вы видите в тексте книги. После этого запустите проект на выполнение (Ctrl+F5):
#import "C:\MyProjects\MyComTLib\Debug\ MyComTLib.tlb" \
no_namespace named_guids
void main()
{
Colnitialize(0);
//====== Используем "умный" указатель
ISayPtr pSay(CLSID_CoSay);
pSay->Say();
pSay->SetWord(L"The client now uses smart pointers!");
pSay->Say();
pSay=0;
CoUninitialize();
}
Несмотря на то что здесь нет многих строчек кода, присутствовавшего в предыдущей версии клиентского приложения, новая версия тоже должна работать. Попробуем разобраться в том, как это происходит.
Примечание 1
Примечание 1
Директивой tfimport можно пользоваться для генерации кода не только на основе TLB-файлов, но также и на основе других двоичных файлов, например ЕХЕ-, DLL- или OCX-файлов. Важно, чтобы в этих файлах была информация о типах СОМ-объекте в.
Вы можете увидеть результат воздействия директивы #import на плоды работы компилятора C++ в папке Debug. Там появились два новых файла заголовков: MyCoTLib.tlh (type library header) и MyComTLib.tli (type library implementations). Первый файл подключает код второго (именно в таком порядке) и они оба компилируются так, как если бы были подключены директивой #include. Этот процесс конвертации двоичной библиотеки типов в исходный код C++ дает возможность решить довольно сложную задачу обнаружения ошибок при пользовании данными о СОМ-объекте. Ошибки, присутствующие в двоичном коде, трудно диагностировать, а ошибки в исходном коде выявляет и указывает компилятор. В данный момент важно не потерять из виду цепь преобразований:
Немного позже мы рассмотрим содержимое новых файлов, а сейчас обратите внимание на то, что директива # import сопровождается двумя атрибутами: no_namespace и named_guids, которые помогают компилятору создавать файлы заголовков. Иногда содержимое библиотеки типов определяется в отдельном пространстве имен (namespace), чтобы избежать случайного совпадения имен. Пространство имен определяется в контексте оператора library, который вы видели в IDL-фай-ле. Но в нашем случае пространство имен не было указано, и поэтому в директиве #import задан атрибут no_namespace. Второй атрибут (named_guids) указывает компилятору, что надо определить и инициализировать переменные типа GUID в определенном (старом) стиле: ывю_муСот, CLSiD_CoSay и iio_isay. Новый стиль задания идентификаторов заключается в использовании операции _uuidof(expression). Microsoft-расширение языка C++ определяет ключевое слово _uuidof и связанную с ним операцию. Она позволяет добыть GUID объекта, стоящего в скобках. Для ее успешной работы необходимо прикрепить GUID к структуре или классу. Это действие выполняют строки вида:
struct declspec(uuid("9b865820-2ffa-1Id5-98b4-00e0293f01b2")) /* LIBID */ _MyCom;
которые также используют Microsoft-расширение языка C++ (declspec). Рассматриваемые новшества вы в изобилии увидите, если откроете файл MyCoTLib.tlh:
// Created by Microsoft (R) C/C++ Compiler.
//
// d:\my projects\saytlibclient\debug\MyComTLib.tlh
//
// C++ source equivalent of Win32 type library
// D:\My Projects\MyComTLib\Debug\MyComTLib.tlb
// compiler-generated file. - DO NOT EDIT!
#pragma once
#pragma pack(push, 8)
#include
//
// Forward references and typedefs //
struct __declspec(uuid("0934da90-608d-4107
-9eccc7e828ad0928"))
/* LIBID */ _MyCom; struct /* coclass */ CoSay;
struct _declspec(uuid("170368dO-85be
-43af-ae71053f506657a2"))
/* interface */ ISay;
{
//
// Smart pointer typedef declarations //
_COM_SMARTPTR_TYPEDEF(ISay, _uuidof(ISay));
//
// Type library items
//
struct _declspec(uuid("9b865820-2ffa
-lld5-98b4-00e0293f01b2"))
CoSay;
// [ default ] interface ISay
struct _declspec(uuid("170368dO-85be
-43af-ae71-053f506657a2")) ISay : lUnknown
{
//
// Wrapper methods for error-handling
//
HRESULT Say ( ) ;
HRESULT SetWord (_bstr_t word ) ;
//
// Raw methods provided by interface -
//
virtual HRESULT _stdcall raw_Say ( ) = 0;
virtual HRESULT _stdcall raw_SetWord
( /*[in]*/ BSTR word ) = 0;
};
//
// Named GUID constants initializations
//
extern "C" const GUID _declspec(selectany)
LIBID_MyCom =
{Ox0934da90, Ox608d, 0x4107,
{.Ox9e, Oxcc, Oxc7, Oxe8, 0x28, Oxad, 0x09, 0x28} } ;
extern "C" const GUID __declspec(selectany) CLSID_CoSay =
{Ox9b865820,0x2ffa,OxlId5,
{0x98,Oxb4,0x00,OxeO,0x29,Ox3f,0x01,Oxb2}};
extern "C" const GUID __declspec(selectany) IID_ISay =
{
0xl70368dO,Ox85be,0x43af,
{0xae,0x71,0x05,Ox3f,0x50,Охбб, 0x57,Oxa2}
};
//
// Wrapper method implementations //
#include "c:\myprojects\saytlibclient
\debug\MyComTLib.tli"
#pragma pack(pop)
Код TLH-файла имеет шаблонную структуру. Для нас наибольший интерес представляет код, который следует после упреждающих объявлений регистрируемых объектов. Это объявление специального (smart) указателя:
_COM_SMARTPTR_TYPEDEF(ISay, _uuidof(ISay));
Для того чтобы добавить секретности, здесь опять использован макрос, который при расширении превратится в:
typedef _com_ptr_t<_com_IIID
Как вы, вероятно, догадались, лексемы _com_lliD и com_ptr_t представляют собой шаблоны классов, первый из них создает новый класс C++, который инкапсулирует функциональность зарегистрированного интерфейса ISay, а второй — класс указателя на этот класс. Операция typedef удостоверяет появление нового типа данных ISayPtr. Отныне объекты типа ISayPtr являются указателями на класс, скроенный по сложному шаблону. Цель — избавить пользователя от необходимости следить за счетчиком ссылок на интерфейс isay, то есть вызывать методы AddRef и Release, и устранить необходимость вызова функции CoCreatelnstance. Заботы о выполнении всех этих операций берет на себя новый класс. Он таким образом скрывает от пользователя рутинную часть работы с объектом СОМ, оставляя лишь творческую. В этом и заключается смысл качественной характеристики smart pointer («сообразительный» указатель).
Характерно также то, что методы нашего интерфейса (Say и SetWord) заменяются на эквивалентные виртуальные методы нового шаблонного класса (raw_say и raw_setword). Сейчас уместно вновь проанализировать код клиентского приложения и постараться увидеть его в новом свете, зная о существовании нового типа ISayPtr. Теперь становится понятной строка объявления:
ISayPtr pSay (CLSID_CoSay);
которая создает объект pSay класса, эквивалентного типу ISayPtr. При этом вызывается конструктор класса. Начиная с этого момента вы можете использовать smart указатель pSay для вызова методов интерфейса ISay. Рассмотрим содержимое второго файла заголовков MyComTLib.tli:
// Created by Microsoft (R) C/C++ Compiler.
//
// d:\my projects\saytlibclient\debug\MyComTLib.tli
//
// Wrapper implementations for Win32 type library
// D:\My Projects\MyComTLib\Debug\MyComTLib.tlb
// compiler-generated file. - DO NOT EDIT!
#pragma once
//
// interface ISay wrapper method implementations
//
inline HRESULT ISay::Say ( )
HRESULT _hr = raw_Say();
if (FAILED(_hr))
_com_issue_errorex(_hr, this,_uuidof(this));
return _hr;
inline HRESULT ISay : :SetWord ( _bstr_t word )
{
HRESULT _hr - raw_SetWord(word) ;
if (FAILED (_hr) )
_com_issue_errorex (_hr, this, _ uuidof (this) );
return _hr;
}
Как вы видите, здесь расположены тела wrapper-методов, заменяющих методы нашего интерфейса. Вместо прямых вызовов методов Say и Setword теперь будут происходить косвенные их вызовы из функций-оберток (raw_Say и raw_SetWord), но при этом исчезает необходимость вызывать методы Createlnstance и Release. Подведем итог. СОМ-интерфейс первоначально представлен в виде базового абстрактного класса, методы которого раскрываются с помощью ко-класса. При использовании библиотеки типов некоторые из его чисто виртуальных функций заменяются на не виртуальные inline-функции класса-обертки, которые внутри содержат вызовы виртуальных функций и затем проверяют код ошибки. В случае сбоя вызывается обработчик ошибок _com_issue_errorex. Таким образом smart-указатели помогают обрабатывать ошибки и упрощают поддержку счетчиков ссылок.
Примечание 2
Примечание 2
В рассматриваемом коде использован специальный miacc_bstr_t предназначенный для работы с Unicode-строками. Он является классом-оберткой для BSTR, упрощающим работу со строками типа B.STR. Теперь можно не заботиться о вызове функции SysFreeString, так как эту работу берет на себя класс _bstr_t.
Разработка клиентского приложения
Разработка клиентского приложения
Для разработки минимального приложения, способного найти DLL COM inproc-сервер, можно начать с заготовки простого приложения консольного типа, инициализировать системные COM DLL и обратиться к ним с просьбой найти наш СОМ-объект и загрузить DLL в адресное пространство нашего процесса. Все это делается при вызове функции CoGetclassObject из семейства сом API. Обратите внимание на то, что нам не надо изменять настройки проекта (Project > Settings) и указывать компоновщику на необходимость подключения DLL, а также указывать ее локальный или сетевой адрес. Собственно, в этом и есть главная заслуга СОМ. Приложение-клиент можно перенести на другую машину, и если там зарегистрирован наш СОМ-объект, то он будет найден и правильно загружен. Функция CoGetclassObject одновременно с поиском и загрузкой DLL СОМ-серве-ра возвращает адрес запрошенного интерфейса. В нашем случае — это isay. Имея адрес интерфейса, можно обращаться к его методам, управляя, таким образом, объектом.
#include "interfaces.h"
void main ()
{
//====== Инциализация COM Library
Colnitialize(0);
//====== Сюда хотим записать адрес интерфейса
ISay *pSay;
// Пытаемся найти и загрузить СОМ DLL-сервер, а также
// получить адрес вложенного интерфейса, указав
// два уникальных идентификатора CLSID_CoSay и IID_ISay
HRESULT hr = CoGetClassObject (CLSID_CoSay,
CLSCTX_INPROC_SERVER, 0, IID_ISay, (void**)&pSay);
if (FAILED(hr))
{
MessageBox(0,"Could not get class object!
", "CoGetClassObject",MB_OK);
CoUninitialize();
return;
}
//====== В случае успеха командуем объектом
pSay->Say();
BSTR word = SysAllocString(L"I hear you well");
pSay->SetWord(word);
SysFreeString(word);
pSay->Say();
//====== Освобождаем интерфейс
pSay->Release();
//====== Закрываем и выгружаем COM Library
CoUninitialize();
}
Запустите приложение (Ctrl+F5), и если вы не допустили какой-либо неточности, то должны увидеть окно сообщения со строкой Hi, there.... После нажатия клавиши Enter должно появиться другое окно с текстом I hear you well. Этот текст задан клиентским приложением, а воспринят и воспроизведен СОМ-объектом. Если объект не работает, то терпеливо проверьте все этапы создания сервера. В модели СОМ существует довольно много мест, где можно допустить ошибку. Наиболее вероятны ошибки в процессе регистрации.
Схема коммуникации клиентсервер
Рисунок 8.1. Схема коммуникации клиент-сервер
СОМ не накладывает ограничений на структуру компонентов, он определяет лишь порядок их взаимодействия. В основе межпроцессной коммуникации лежит все та же косвенная адресация (таблица виртуальных функций), которая позволяет передать управление либо прямо методу интерфейса (inproc-server), либо его представителю (proxy) на стороне клиента, который, в свою очередь, делает RPC (удаленный вызов) метода настоящего объекта. Прозрачность СОМ-объекта для клиента заключается в том, что proxy-объект знает, где расположен реальный объект (на другом компьютере — remote server, или на том же самом — local server), а клиент об этом не знает.
Когда клиент хочет использовать СОМ-сервер, он обращается к системной библиотеке с просьбой найти и загрузить сервер, чей CLSID равен определенному значению. Заодно клиент передает IID требуемого интерфейса. В ответ на это системная COM DLL вызывает SCM (Service Control Manager) — менеджер сервисов, который запускается во время загрузки системы. SCM является RFC-сервером, который использует системный реестр, чтобы выполнить поиск реализации, то есть отыскать ЕХЕ- или DLL-файл, содержащий требуемый СОМ-сервер. Чтобы найти модуль сервера, SCM ищет в реестре его CLSID. Если он найден, то SCM возвращает связанный с ним файловый путь, а СОМ загружают этот модуль в память. Теперь возможны два варианта действий: если сервер находится в ЕХЕ-файле, то СОМ запускает его, устанавливает канал связи (RPC) между представителями клиента и сервера (proxy/stub) и возвращает интерфейсный указатель клиенту. Если СОМ-сервер находится в DLL-файле, СОМ просто передаст клиенту указатель на фабрику классов сервера.
Создание элемента типа ATL Control
Создание элемента типа ATL Control
Создаваемый модуль DLL будет содержать в себе элемент управления, который внедряется в окно клиентского приложения, поэтому в проект следует добавить заготовку нового СОМ-класса, обладающего функциональностью элемента типа ATL Control. В следующем уроке мы внесем в него функциональность окна OpenGL, поэтому мы назовем класс OpenGL, хотя в этом уроке элемент не будет иметь дело с библиотекой Silicon Graphics. Он будет элементом ActiveX, созданным на основе заготовки ATL. Создать вручную элемент ActiveX достаточно сложно, поэтому воспользуемся услугами еще одного мастера Studio.Net. При включении нового мастера (wizard) важно, где установлен фокус. Отметьте, что сейчас в рабочем пространстве существуют два проекта: один (ATLGL) — это DLL-сервер, а другой (ATLGLPS) — это коды заглушек proxy/stub.
Просмотрите результаты работы мастера. Самым крупным его произведением является файл OpenGLh, который содержит объявление и одновременно коды класса COpenGL. Для ATL-проектов характерно то, что создаваемые ко-классы наследуют данные и методы от многих родителей, в число которых входят как СОМ-классы, так и интерфейсы. Другой характерной чертой является сосредоточение значительной части функциональности в h-файле. Напрашивается вывод, что некоторые принципы и идеи, отстаиваемые Microsoft в MFC, были инвертированы в ATL. Сколько полемического задора было растрачено в критике множественного наследования (намек на Borland OWL) на страницах документации по MFC, и вот теперь мы видим вновь созданный класс (COpenGL), который имеет 18 родителей, среди которых 5 классов и 13 интерфейсов.
Здесь у вас опять должна закружиться голова, но не сдавайтесь. Важно не выпускать главную нить логики приложения. Резон таков: мастера настрочили уйму кода, который пока непонятен, возможно, и всегда будет таким, но этот код уже работает и нам нужно где-то встроиться в него, чтобы почувствовать возможность управлять общей логикой внедряемого элемента ActiveX. Имея под рукой Wizards Studio.Net, это можно сделать, даже оставаясь в некотором неведении относительно деталей работы интерфейсов СОМ. Вам не придется вручную реализовывать ни одного интерфейса. Вы можете сосредоточиться только на алгоритме работы самого элемента, то есть на том, что вы должны продемонстрировать пользователю вашего объекта.
Запустите приложение, но на этот раз не закрывайте тестовый контейнер, который должен запуститься автоматически, без вашего участия. В окне тестового контейнера вы не увидите признаков нашего элемента, так как он еще не загружен. Дайте команду Edit > IhsertNew Control. После некоторой паузы, в течение которой контейнер собирает информацию из реестра обо всех элементах OLE Controls, вы увидите диалоговое окно с длинным списком элементов, о которых есть информация в реестре.
Примечание 1
Примечание 1
Это совсем не означает, что все элементы живы и здоровы. На мой взгляд, ситуация уже вырастает в серьезную проблему. В систему следует ввести эффективные средства корректировки реестра, потому что совсем неинтересно проводить часы драгоценного времени, копаясь в реестре или инструменте типа OLE/COM Object Viewer (Просмотр объектов OLE/COM) и выясняя, жив элемент или его давно нет. Может быть, как говорят политики, я не владею информацией, но все программки типа CleanRegistry либо опасны, либо мало полезны и неэффективны.
При открытом окне диалога Insert Control вы можете просто ввести букву о — начальную букву нашего элемента OpenGL. Теперь, используя клавиши навигации по списку (стрелки), быстро найдете в нем строку OpenGL Class. Выберите ее и нажмите ОК. Вы должны увидеть окно внедренного элемента, которое выглядит так, как показано на Рисунок 8.2.
Создание класса СОМобъекта
Создание класса СОМ-объекта
Подключите к проекту новый файл MyCom.h, в который надо поместить объявление класса CoSay. Как вы помните, он должен быть потомком экспортируемого интерфейса iSay и дать тела всем методам, унаследованным от всех своих абстрактных предков (isay, lUnknown). Введите в файл следующие коды:
#if !defined(MY_COSAY_HEADER)
#define MY_COSAY_HEADER
#pragma once
class CoSay : public ISay
{
//=====Класс, реализующий интерфейсы ISay, lUnknown
public:
CoSay () ;
virtual -CoSay();
// lUnknown
HRESULT _stdcall Querylnterface(REFIID riid, void** ppv);
ULONG _stdcall AddRefO;
ULONG _stdcall Release ();
// ISay
HRESULT _stdcall Say();
HRESULT _stdcall SetWord (BSTR word);
private:
//====== Счетчик числа пользователей классом
ULONG m_ref; , //====== Текст, выводимый в окно
BSTR m word;
};
#endif
Для реализации тел методов класса CoSay подключите к проекту новый файл МуСоm. срр, в который введите коды, приведенные ниже. Обратите внимание на то, как принято работать со строками текста типа BSTR:
#include "interfaces.h"
#include "MyCom.h"
//====== Произвольный ограничитель длины строк
#define MAX_LENGTH 128
CoSay::CoSay()
{
//=== Обнуляем счетчик числа пользователей класса,
//=== так как интерфейс пока не используется
m_ref = 0;
//=== Динамически создаем строку текста по умолчанию
m_word = SysAllocString (L"Hi, there."
"This is MyCom speaking");
}
CoSay::-CoSay()
{
//=== При завершении работы освобождаем память
if (m_word)
SysFreeString(m_word);
}
//====== Реализация методов lUnknown
HRESULT _stdcall CoSay::QueryInterface(REFIID riid, void** ppv)
{
//====== Стандартная логика работы с клиентом
//====== Поддерживаем только два интерфейса
*ppv = 0;
if (riid==IID_IUnknown)
*ppv = static_cast
else if (riid==IID_ISay)
*ppv = static_cast
else
return E_NOINTERFACE;
//====== Есть пользователи нашим объектом
AddRef();
return S_OK;
}
ULONG _stdcall CoSay:-.AddRef ()
{
return ++m_ref;
}
ULONG _stdcall CoSay::Release()
{
if (--m_ref==0) delete this;
return m_re f;
}
//====== Реализация методов ISay
HRESULT _stdcall CoSay::Say()
{
//=== Преобразование типов (из BSTR в char*), которое
//=== необходимо для использования MessageBox
char buff[MAX_LENGTH];
WideCharToMultiByte(CP_ACP, 0, m_word, -1, buff, MAX_LENGTH, 0, 0);
MessageBox (0, buff, "Interface ISay:", MB_OK);
return S_OK;
}
HRESULT _stdcall CoSay::SetWord(BSTR word)
{
//====== Повторное выделение памяти
SysReAllocString (&m_word, word);
freturn S_OK;
}
Класс, поддерживающий интерфейс, готов. Теперь следует сделать доступным для пользователей СОМ-объекта весь DLL-сервер, где живет ко-класс CoSay. Минимальным набором функций, которые должна экспортировать COM DLL, является реализация только одной функции DllGetClassObject. Обычно ее сопровождают еще три функции, но в данный момент мы рассматриваем лишь минимальный набор. DLL должна создать СОМ-объект и позволить работать с ним, получив, то есть записав по адресу ppv, адрес зарегистрированного интерфейса. Вы, конечно, заметили, что в предложении дважды использовано слово адрес. Именно поэтому параметр ppv имеет тип void** . Введите эту функцию в конец файла МуСот.срр:
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv)
{
//=== Если идентификатор класса задан неправильно,
if (rclsid != CLSID_CoSay)
// возвращаем код ошибки с указанием причины неудачи
return CLASS_E_CLASSNOTAVAILABLE;
//====== Создаем объект ко-класса
CoSay *pSay = new CoSay;
//=== Пытаемся получить адрес запрошенного интерфейса
HRESULT hr = pSay->Query!nterface (riid, ppv) ;
if (FAILED(hr))
delete pSay;
return hr;
}
Макроподстановка STDAPI при разворачивании превратится в
extern "С" HRESULT stdcall
Примечание 1
Примечание 1
Работа по опознаванию объектов идет с идентификаторами класса (rclsid) и интерфейса (riid). Это является, как считают апологеты СОМ, одной из самых важных черт, которые вносят небывалый уровень надежности в функционирование СОМ-приложений. Весьма спорное утверждение, так как центром всей вселенной как разработчика, так и пользователя становится Windows-реестр, который открыт всем ветрам — как случайным, так и преднамеренным воздействиям со стороны человека и программы. Однако следует согласиться с тем, что уникальная идентификация снимает проблему случайного, но весьма вероятного совпадения имен интерфейсов, разработанных в разных частях света. То же относится и к именам классов, библиотек типов и т. д.
Стартовая заготовка элемента ActiveX
Рисунок 8.2. Стартовая заготовка элемента ActiveX в окне тестового контейнера
Загляните в файл ATLGLJ.c и увидите три новых макроса типа MIDL_DEFINE_GUID, которые уже выполнили свою работу и поместили в реестр множество новых записей по адресам:
HKEY_CLASSES_ROOT\ATLGL.OpenGL\
HKEY_CLASSES_ROOT\ATLGL.OpenGL.1\
HKEY_CLASSES_ROOT\CLSID\
HKEY_CLASSES_ROOT\ Interface\
Когда клиент СОМ-объекта пользуется услугами локального или удаленного сервера, то есть когда данные передаются через границы различных процессов или между узлами сети, требуется поддержка маршалинга (marshaling). Так называется процесс упаковки и посылки параметров, передаваемых методам интерфейсов через границы потоков или процессов, который мы слегка затронули ранее. Вы помните, что MIDL генерирует код на языке С для двух компонентов: Proxy (представитель СОМ-объекта на стороне клиента) и stub (заглушка на стороне СОМ-сервера). Эти компоненты общаются между собой и решают проблемы Вавилонской башни, то есть преодолевают сложности обмена данными, возникающими из-за того, что клиент и сервер используют различные типы данных — разговаривают на разных языках. Чтобы увидеть проблему, надо ее создать. Интересно то, что при объяснении необходимости этого чудовищного сооружения:
приводится соображение о том, что программы на разных языках программирования смогут общаться, то есть обмениваться данными. Как мы уже обсуждали, разработчики имеют в виду четыре языка, два из которых реально используются (Visual C++ и Visual Basic), а два других (VBScript и Visual J++) едва подают признаки жизни. Правда здесь надо учесть бурное развитие нового языка с#, который, очевидно, тоже участвует в движении СОМ.
Откройте файл ATLGLidl и постарайтесь вникнуть в смысл новых записей, не отвлекаясь на изучение языка IDL, который потребует от вас заметных усилий и временных затрат. Прежде всего отметьте, что в библиотеке типов (library ATLGLLib), сопровождающей наш СОМ-объект, появилось описание СОМ-класса
coclass OpenGL
{
[default] interface IQpenGL;
[default, source] dispinterface _IOpenGLEvents;
};
который предоставляет своим пользователям два интерфейса. Я не привожу здесь предшествующий классу OpenGL блок описаний в квадратных скобках, который носит вспомогательный характер. Элементы ActiveX используют события (events) для того, чтобы уведомить приложение-контейнер об изменениях в состоянии объекта в результате действий пользователя — манипуляции посредством мыши и клавиатуры в окне объекта. Найдите описание одного из объявленных интерфейсов:
dispinterface _IOpenGLEvents
{
properties:
methods:
};
Пока пустые секции properties (свойства): и methods (методы): намекают на то, что мы должны приложить усилия и ввести, с помощью инструментов Studio.Net в разрабатываемый СОМ-объект способность изменять свои свойства и экспортировать методы. Информация о втором интерфейсе расположена вне блока, описывающего библиотеку типов:
interface IQpenGL : 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);
};
Уникальная идентификация объектов
Уникальная идентификация объектов
Данные типа GUID (globally unique identifier) являются 128-битными идентификаторами, состоящими из пяти групп шестнадцатеричных цифр,' которые обычно генерирует специальная программа uuidgen, входящая в инструменты Studio.Net. Например, если вы в командной строке Windows наберете
uuidgen -n2 -s >guids.txt
то в файле guids.txt получите два уникальных числа вида:
{12340001-4980-1920-6788-123456789012}
{1234*0002-4980-1920-6788-123456789012}
которые можно использовать в качестве ключа регистрации в Windows-реестре. Рекомендуется обращаться к утилите uuidgen и просить сразу много идентификаторов, а затем постепенно использовать их (помечая, чтобы не забыть) в своем приложении для идентификации интерфейсов, СОМ-классов и библиотек типов. Это упрощает отладку, поиск в реестре и, возможно, его чистку. Кроме этого способа существуют и другие. Например, можно обратиться к функции
HRESULT CoCreateGuid(GUID *pguid);
которая гарантированно выдаст уникальное 128-битное число, которое не совпадет ни с одним другим числом, полученным в любой вычислительной системе, в любой точке планеты, в любое время в прошлом и будущем. Впечатляюще, не правда ли? Есть целая серия функций вида Uuid* из блока RFC-API, которые генерируют и обрабатывают числа типа GUID. Число, как вы видите, разбито на пять групп, как-то связанных с процессом генерации, в котором задействованы время генерации, географическое место, информация о системе и т. д. Следующие типы переменных эквивалентны типу GUID:
Тип IID используется также и для идентификации библиотек типов. Переменные типа GUID являются структурами, содержащими четыре поля. Тип GUID определен в guiddef.h следующим образом:
typedef struct
{
//=== 1-я группа цифр (8 цифр - 4 байта)
unsigned long Datal;
//=== 2-я группа цифр (4 цифры - 2 байта)
unsigned short Data2;
//=== 3-я группа цифр (4 цифры - 2 байта)
unsigned short Data3;
//=== 4-я и 5-я группы (4 и 12 цифр) - 8 байт
byte Data4[8];
}
GUID;
Мы уже обсуждали необходимость уникальной идентификации интерфейсов. Ну а зачем уникально идентифицировать классы? Предположим, что два разработчика создали два разных СОМ-класса, но оба назвали их MySuperGrid. Так как СОМ узнает класс по его CLSID, а алгоритм генерации CLSID гарантирует его уникальность, то совпадение имен не мешает использовать оба класса в одном клиентском приложении. Система пользуется двумя типами GUID: строковым (применяется в реестре) и числовым (нужен клиентским приложениям).
Я думаю, что в этот момент у неискушенного СОМ-технологией читателя должна слегка закружиться голова. Это нормально, так как по заявлению авторитетов (David Cruglinsky), она будет кружиться в течение примерно полугода, при условии регулярного изучения СОМ-технологий.
Загадочные макросы
Загадочные макросы
Вернемся в файл ATLGLcpp, где кроме функций, перечисленных выше, присутствуют загадочные макросы. Их смысл довольно прозрачен, но разработчика не должны устраивать догадки, ему нужны более точные знания. Сопровождающая документация, особенно бета-версий, не всегда дает нужные объяснения, поэтому приходится искать их самостоятельно в заголовочных файлах, расположенных по адресу: ...\Microsoft Visual Studio.Net\Vc7\indude или ...\Microsoft Visual Studio.Net\ Vc7\atlmfc\include.
Покажем, как это делается на примере. Нас интересует смысл функциеподобной макроподстановки:
DECLARE_LIBID(LIBID_ATLGLLib)
В результате поиска в файлах по указанному пути (маска поиска должна быть *.h) находим (в файле ATLBase.h), что при разворачивании препроцессором этот макрос превратится в статическую функцию класса CATLGLModule:
static void InitLibldO throw ()
{
CAtlModule::m_libid = LIBID_ATLGLLib;
}
Теперь возникает желание узнать, что кроется за идентификатором LiBiD_ATLGLLib. Во вновь созданном коде файла ATLGM.c находим макрос:
MIDL_DEFINE_GUID(IID,
LIBID_ATLGLLib,ОхЕбОбОЗВС,Ox9DE2, 0x4563,
OxA7,0xAF,Ox8A,Ox8C,Ox4E,0x80,0x40,0x58);
узнав смысл которого мы сможем понять, чем является LiBiD_ATLGLLib. В вашем проекте цифры будут другими, но я привожу здесь те, которые вижу сейчас, для того чтобы быть более конкретным и не загружать вас абстракциями, которых и так хватает. В этом случае поиск не нужен, так как объявление макроса расположено двумя строчками выше. Вот оно:
#define MIDL_DEFINE_GUID(type,name,1,wl,w2,bl,b2,b3,Ь4, \ Ь5,Ьб,b7,b8)
const type name = \ {I,wl,w2, {b1,b2,bЗ,b4,b5,b6,b7,b8}
}
Подставив значения параметров из предыдущего макроса, получим определение LiBiD_ATLGLLib, которое увидит компилятор:
const IID LIBID_ATLGLLib =
{
0xE60605BC, 0x9DE2, 0x4563,
{ 0xA7,0xAF,0x8A, 0x8C,Ox4E, 0x80, 0x40, 0x58 }
}
Отсюда ясно, что LIВID_АТLGLLib — это константная структура типа IID. Осталось узнать, как определен тип данных II D.
В хорошо знакомом файле afxwin.h находим определение typedef GUID IID;. Про Globally Unique Identifier (GUID) сказано очень много, в том числе и в документации Studio.Net. Как мы только что выяснили, изучив работу макросов и LiBio_ATLGLLib, тип IID также используется для идентификации библиотек типов. Система применяет два типа GUID: строковый в реестре, и числовой в клиентских приложениях. Второй макрос, который вы видели в классе
CATLGLModule:
DECLARE_REGISTRY_APPID_RESOURCEID(IDR_ATLGL,
"{E4541023-7425-4AA7-998C-D016DF796716}")
(цифры мои, ваши будут другими) создает строковый GUID. При расширении он превратится в три статические функции класса, две из которых готовят текстовую строку того или иного типа, а третья регистрирует, в случае если bRegister==TRUE, или убирает из реестра эту строку по адресу HKEY_CLASSES_ROOT\APPID\:
static LPCOLESTR GetAppId ()throw ()
{
//====== Преобразование к формату OLE-строки
return OLESTR("{E4541023-7425-4AA7-998C-D016DF796716}") ;
}
static TCHAR* GetAppIdTO throw ()
{
//====== Преобразование к Unicode или char* строке
return _T("{E4541023-7425-4AA7-998C-D016DF796716}") ;
}
// Если bRegister==TRUE, то происходит запись в реестр,
// иначе - удаление записи
static HRESULT WINAPI UpdateRegistryAppId(BOOL bRegister) throw()
{
_ATL_REGMAP_ENTRY aMapEntries [] =
{
{ OLESTRC'APPID") , GetAppIdO }, { NULL, NULL }
};
return ATL::_pAtlModule->UpdateRegistryFromResource( IDR ATLGL, bRegister, aMapEntries);
В данный момент вы сможете найти в реестре свой ключ и ассоциированную с ним строку (ATLGL) по адресу:
HKEY_CLASSES_ROOT\AppID\
{E4541023-7425-4AA7-998C-D016DF796716}
При запуске приложения вышеописанные функции были вызваны каркасом приложения и произвели записи в реестр. Отметьте также, что в реестре появилась еще одна (симметричная) запись по адресу HKEY_CLASSES_ROOT \APPID\ATLGL.DLL. Она ассоциирует строковый GUID с библиотекой ATLGL.DLL. Рассматриваемая строка-идентификатор встречается еще в нескольких разделах проекта, найдите их, чтобы получить ориентировку: в ресурсе "REGISTRY" > IDR_ATLGL (см. окно Resource View) и в файле сценария регистрации ATL.GL.rgs (см. окно Solution Explorer).
Возвращаясь к первому макросу DECLARE_LIBID(LiBiojvTLGLLib), отметим, что скрытая за ним функция initLibid тоже была вызвана каркасом и использована для регистрации библиотеки типов будущего СОМ-объекта. Вы можете найти эту, значительно более подробную, запись по ключу (цифры мои):
HKEY_CLASSES_ROOT\TypeLib\
{E60605BC-9DE2-4563-A7AF-8A8C4E804058}
Трехмерная графика в проекте ATL
Файловые операции
Файловые операции
Создание тестовой поверхности, чтение данных из файла и хранение этих данных в контейнере мы будем делать так же, как и в проекте MFC. Для разнообразия используем другую формулу для описания поверхности по умолчанию, то есть того графика, который увидит пользователь элемента ActiveX при его инициализации в рамках окна контейнера. Вот эта формула:
Приведем тело функции 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
for (UINT j=0; j
*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.
Классоболочка
Класс-оболочка
Обычно при создании приложения-контейнера для элемента ActiveX придерживаются следующей стратегии:
Первый шаг этого алгоритма вы уже выполнили, теперь введите в состав проекта два новых файла OpenGLh и OpenGLcpp, которые будут содержать коды класса-оболочки copenGL. Вот содержимое файла заголовков (OpenGLh):
#pragma once
//=========== COpenGL wrapper class
class COpenGL : public CWnd
{
protected:
DECLARE_DYNCREATE(COpenGL)
public:
//==== Метод для добывания CLSID нашего элемента
CLSID const& GetClsidO
{
static CLSID const clsid =
{
0x519d9ed8, Oxbc4'6, 0x4367,
{ Ox9c, OxcO, 0x49, 0x81, 0x40, Oxf3, 0x94, 0x16 }
};
return clsid;
}
virtual BOOL Create(LPCTSTR IpszClassName,
LPCTSTR IpszWindowName, DWORD dwStyle,
const RECT& rect, CWnd* pParentWnd, UINT nID, CCreateContext* pContext = NULL)
{
return CreateControl(GetClsid(), IpszWindowName,
dwStyle, rect, pParentWnd, nID)
}
BOOL Create (LPCTSTR IpszWindowName, DWORD dwStyle,
const RECT& rect, CWnd* pParentWnd, UINT nID, CFile* pPersist = NULL,
BOOL bStorage = FALSE, BSTR bstrLicKey = NULL)
{
return CreateControl(GetClsidO, IpszWindowName, dwStyle, rect, pParentWnd, nID, pPersist, bStorage, bstrLicKey);
}
//====== Методы, экспонируемые элементом ActiveX
public:
void SetFillColor(unsigned long newValue);
unsigned long GetFillColor();
void GetLightParams(long* pPos);
void SetLightParam(short Ip, long nPos);
void ReadData();
void SetFillMode(DWORD mode);
void GetFillMode(DWORD* pMode);
void GetQuad(BOOL* bQuad);
void SetQuad(BOOL bQuad);
};
Самым важным моментом в процедуре вставки класса является правильное задание CLSID того класса OpenGL, который был зарегистрирован в операционной системе при создании DLL-сервера, то есть нашего элемента ActiveX. He пытайтесь сравнивать те цифры, которые приведены в книге, с теми, которые были приведены в ней же до этого момента, так как в процессе отладки пришлось не раз менять как классы, так и целиком приложения. Мне не хочется отслеживать эти жуткие номера. Если вы хотите вставить правильные цифры, то должны взять их из вашей версии предыдущего приложения ATLGL. Например, откройте файл ATLGL.IDL и возьмите оттуда CLSID для ко-класса OpenGL, то есть найдите такой фрагмент этого файла:
[
uuid(519D9ED8-BC46-4367-9CCO-498140F39416),
helpstring("OpenGL Class") ]
coclass OpenGL
{
[default] interface IOpenGL;
[default, source] dispinterface _IOpenGLEvents;
};
И скопируйте первую строку
uuid(519D9ED8-BC46-4367-9CCO-498140F39416),
но с вашими цифрами и вставьте ее в качестве комментария в файл OpenGLh нового проекта TestGL. Затем аккуратно, соблюдая формат, принятый для структуры CLSID, перенесите только цифры в поля статической структуры clsid, которую вы видите в методе GetClsid класса-оболочки. Цифры должны быть взяты из принесенной строки, но их надо отформатировать (разбить) по-другому принципу. Например, для нашего случая правильным будет такое тело метода GetClsid:
CLSID const& GetClsid()
{
// Следующая строка взята из файла ATLGL.IDL
// 519D9ED8-BC46-4367-9CCO-498140F39416
static CLSID const clsid =
{
//======== Эти цифры взяты из файла ATLGL.IDL
0x519d9ed8, 0xbc46, 0x4367,
{ 0х9с, 0xc0, 0x49, 0x81, 0x40, 0xf3, 0x94, 0x16 } ) ;
return clsid;
}
Кроме этого важного фрагмента в новом классе объявлены два совмещенных метода Create, каждый из которых умеет создавать окно внедренного элемента ActiveX с учетом особенностей стиля окна (см. справку по CWnd: :CreateControl). Затем в классе-оболочке должны быть представлены суррогаты всех методов, экспонируемых классом OpenGL COM DLL-сервера ATLGL.DLL. В том, что вы не полностью приводите тела методов сервера, иначе это был бы абсурд, хотя и так близко к этому, можно убедиться, просмотрев на редкость унылые коды реализации класса-оболочки, которые необходимо вставить в файл OpenGLcpp. Утешает мысль, что в исправной Studio.Net эти коды не придется создавать и редактировать вручную:
#include "stdafx.h"
#include "opengl.h"
IMPLEMENT_DYNCREATE(COpenGL, CWnd)
//====== Стандартное свойство реализовано
//====== в виде пары методов Get/Set
void COpenGL::SetFillColor(unsigned long newValue)
{
static BYTE parms[] =
VTS_I4; InvokeHelper(0xfffffe02, DISPATCH_PROPERTYPUT,VT_EMPTY,
NULL, parms, newValue);
}
//====== Стандартное свойство
unsigned long COpenGL::GetFillColor0 {
unsigned long result;
InvokeHelper (Oxfffffe02, DISPATCH_PROPERTYGET, VT_I4, (void4)&result, NULL);
return result;
}
//====== Наши методы сервера
void COpenGL::GetLightParams(long* pPos)
{
static BYTE parms[] = VTS_PI4;
InvokeHelper (Oxl, DISPATCH_METHOD, VT_EMPTY, NULL,
parms, pPos);
}
void COpenGL: : SetLightParam (short lp, long nPos)
{
static BYTE parms [ ] = VTS 12 VTS 14;
InvokeHelper{0x2, DISPATCH_METHOD, VT_EMPTY, NULL,
parms, lp, nPos);
}
void COpenGL::ReadData()
InvokeHelper(0x3, DISPATCH_METHOD, VT_EMPTY, 0, 0) ;
void COpenGL::GetFillMode(DWORD* pMode)
static BYTE jparms[] =
VTS_PI4; InvokeHelper (0x4, DISPATCH_METHOD, VT_EMPTY, NULL,
parms, pMode);
}
void COpenGL::SetFillMode(DWORD nMode)
static BYTE parms[] =
VTS_I4;
InvokeHelper(0x5, DISPATCH_METHOD, VT_EMPTY, NULL, parms, nMode);
void COpenGL::GetQuad(BOOL* bQuad)
static BYTE parms[] =
VTS_PI4;
InvokeHelper(0x6, DISPATCH_METHOD, VT_EMPTY, NULL, parms, bQuad);
void COpenGL::SetQuad(BOOL bQuad)
static BYTE parms[] =
VTS_I4;
InvokeHelper (0x7, DISPATCH_METHOD, VT_EMPTY, NULL, parms, bQuad);
}
Затем подключите оба новых файла к проекту Project > Add Existing Item.
Конструируем облик страницы свойств
Конструируем облик страницы свойств
Важным моментом в том, что произошло, когда вы добавили страницу свойств, является появление шаблона окна диалоговой вставки IDD_PROPDLG. Сейчас вам следует сконструировать облик этой вставки, разместив на ней элементы управления, необходимые для управления освещением. Кроме того, мы поместим туда кнопку вызова файлового диалога, выпадающий список для выбора одного из трех режимов заполнения полигонов и кнопку для переключения режима генерации поверхности (GL_QUADS или GL_QUAD_STRIP). Создайте с помощью редактора диалогов окно, примерный вид которого приведен на Рисунок 9.2. Вы, наверное, знаете, что нижний ряд кнопок поставляется блоком страниц (property sheet) и вам их вставлять не надо, необходимо сконструировать только облик самой страницы.
Окно ActiveX элемента внедренного
Рисунок 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;
}
Подготовка сцены OpenGL
Подготовка сцены 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
//=== Связанные полигоны начинаются
//=== на каждой полосе вновь if (!m_bQuad)
glBegin(GL_QUAD_STRIP) ;
//=== Цикл прохода вдоль оси X
for (UINT x=0; x
{
// i, j, k, n — 4 индекса вершин примитива при
// обходе в направлении против часовой стрелки
int j = i + m_xSize,
// Индекс узла с большим Z
k = j+1, // Индекс узла по диагонали
n = i+1; // Индекс узла справа
// Выбор координат 4-х вершин из контейнера
float
xi = m_cPoints [i] . х,
yi = m_cPoints [i] .y,
zi = m_cPoints [i] . z,
xj = m_cPoints [ j ] .x,
yj = m_cPoints [ j ] .y,
zj = m_cPoints [ j ] .z,
xk = m_cPoints [k] .x,
yk = m_cPoints [k] . y,
zk = m_cPoints [k] . z,
xn = m_cPoints [n] .x,
yn = m_cPoints [n] .y,
zn = m_cPoints [n] . z,
//=== Координаты векторов боковых сторон
ах = xi-xn,
ay = yi-yn,
by = yj-yi,
bz = zj-zi,
//=== Вычисление вектора нормали
vx = ay*bz,
vy = -bz*ax,
vz = ax*by,
//=== Модуль нормали
v = float (sqrt (vx*vx + vy*vy + vz*vz) ) ;
//====== Нормировка вектора нормали
vx /= v;
vy /= v;
vz /= v;
//====== Задание вектора нормали
glNormalSf (vx,vyfvz);
// Ветвь создания несвязанных четырехугольников
if (m_bQuad)
{
//====== Обход вершин осуществляется
//=== в направлении против часовой стрелки
glColorSf (0.2f, 0.8f, l.f);
glVertex3f (xi, yi, zi);
glColor3f <0.6f, 0.7f, l.f);
glVertexSf (xj, уj, zj);
glColorSf (0.7f, 0.9f, l.f);
glVertexSf (xk, yk, zk);
glColorSf (0.7f, 0.8f, l.f);
glVertexSf (xn, yn, zn); }
else
// Ветвь создания цепочки четырехугольников
{
glColor3f (0.9f, 0..9f, l.Of);
glVertexSf (xi, yi, zi);
glColorSf (0.5f, 0.8f, l.0f);
glVertexSf (xj, уj, zj);
}
}
//====== Закрываем блок команд GL_QUAD_STRIP
if (!m_bQuad)
glEnd(); }
//====== Закрываем блок команд GL_QUADS
if (m_bQuad) glEnd() ;
//====== Закрываем список команд OpenGL
glEndList ();
}
Для осмысления алгоритма надо учитывать, что количество узлов сетки вдоль того или иного направления (X или Z) на единицу больше количества промежутков (ячеек). Кроме того, надо иметь в виду, что при расчете освещения OpenGL учитывает направление нормали (перпендикуляра) к поверхности. Реалистичность изображения во многом достигается благодаря аккуратному вычислению нормалей. Нормаль является характеристикой вершины (узла сетки).
Рисуем четырехугольниками m_bQuad = true;
Рисуем четырехугольниками 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;
}
Ручная коррекция класса
Ручная коррекция класса
Класс COpenGL будет обслуживать окно внедренного СОМ-объекта. Он должен иметь достаточное количество данных и методов для управления изображаемой поверхностью, поэтому далее вручную введем сразу много изменений в файл с описанием класса COpenGL. При изменении файла заголовков класса мы нарушим стиль, заданный стартовой заготовкой, и вернемся к более привычному, принятому в MFC-приложениях. Перенесем существующее тело конструктора, а также функции OnDraw в файл реализации класса OpenGLcpp. В файле OpenGLh останутся только декларации этих функций. Ниже приведено полное описание класса COpenGL с учетом нововведений, упрощений и исправлений. Вставьте его вместо того текста, который есть в файле OpenGLh. После этого вставим в файл новые сущности с помощью инструментов Studio.Net:
// OpenGL.h : Declaration of the COpenGL
#pragma once
#include "resource.h" // main symbols
#include
#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
public CStockPropImpKCOpenGL, IOpenGL>,
public IPersistStreamInitImpl
public I01eControlImpl
public I01eObjectImpl
public I01eInPlaceActiveObjectImpl
public IViewObjectExImpl
public I01eInPlaceObjectWindowlessImpl
public ISupportErrorlnfo,
public IConnectionPointContainerImpl
public CProxy_IOpenGLEvents
public IPersistStorageImpl
public ISpecifyPropertyPagesImpl
public IQuickActivateImpl
public IDataObjectImpl
public IProvideClassInfo2Impl<&CLSID_OpenGL,
&_uuidof(_IOpenGLEvents), &LIBID_ATLGLLib>,
public CComCoClass
public CComControl
{
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
//====== Функции, присутствовавшие в стартовой заготовке
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)
Начальные строки кода класса должны показаться вам знакомыми, так как вы уже знаете, что мастер 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) интерфейсы.
Примечание 1
Примечание 1
Интерфейсы, раскрываемые с помощью рассмотренного механизма 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) сообщение, но не обрабатывают его.
Поддержка этого интерфейса проста. В методе interfaceSupportsErrorinfo имеется статический массив а г г, в котором хранятся адреса идентификаторов вновь создаваемых интерфейсов, пока он у нас один HD_iOpenGL. В этом же методе осуществляется пробег по всему массиву индексов и вызов функции inlinelsEqualGUio, которая пока не документирована, но ее смысл может быть выведен из ее имени.
Этот интерфейс является расширением интерфейса 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, выполняющий сходную функцию, но при этом не нуждается в обрамлении из операторных скобок.
Страницы свойств
Страницы свойств
Перед тем как мы начнем работать с окном СОМ-объекта, вводя в него реакции на управляющие воздействия, покажем, как добавить страницу свойств (property page) в уже существующий блок страниц объекта, который активизируется с помощью контекстного меню. Страница свойств является отдельным элементом управления, называемым Property Page, интерфейсы которого должны быть реализованы в рамках отдельного ко-класса. Такая структура позволяет нескольким ко-классам одновременно пользоваться страницами свойств, размещенными в общем СОМ DLL-сервере. Новый класс для поддержки страницы свойств помещается в сервер с помощью той же процедуры, которую мы использовали при вставке класса COpenGL, но при этом следует выбрать другой тип элемента управления. Вновь воспользуемся услугами мастера Studio.Net ATL Add Class.
Просмотрите результаты. Прежде всего убедитесь, что в проекте появился новый класс 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, которые могут не совпадать даже с предыдущей версией в этой книге, так как в процессе разработки сервера мне приходится неоднократно повторять заново процесс создания ко-классов:
Примечание 1
Примечание 1
Каждый раз при этом идентификаторы 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 загружается в пространство чужого процесса.
Примечание 2
Примечание 2
Имя ICircCtl, которое присутствует в подсказке, не имеет отношения к нашему проекту. Оно связано с учебным примером по созданию элементов управления с помощью библиотеки ATL. Вы можете увидеть этот пример в MSDN (Visual C++ Tutorials > Creating the Circle Control).
Переменная m_bDirty используется каркасом в качестве флага доступности кнопки Apply. Если m_bDirt у == FALSE; то кнопка недоступна. Она тотчас же должна стать доступной, если пользователь страницы диалога свойств введет изменения в органы управления на лице диалога. Конечно, этим состоянием управляет разработчик, то есть мы с вами.
Идентификаторы элементов управления
Таблица 9.1. Идентификаторы элементов управления
Вместо кнопки Quads просится пара переключателей (radio button) Quads/Strip. Сначала я так и сделал, но потом, к сожалению, пришлось отказаться из-за сложностей введения отклика реакции или уведомления, на выбор, произведенный в группе переключателей. Они обусловлены несовершенством бета-версии Studio.Net. Если вы впервые устанавливаете группу переключателей (radio buttons), то вам следует знать, что группа Quads/Strip будет работать правильно, если числовые значения идентификаторов составляющих ее элементов следуют подряд и (только) для первого переключателя установлено свойство Group. Для второго этот флаг должен быть снят. Если вы вставляете еще одну группу, то картина должна повториться. Первый переключатель должен иметь свойство Group в положении True, а остальные (если их много) — нет.
Элемент
Идентификатор
/ Диалог
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
Для того чтобы просмотреть числовые значения идентификаторов, следует поставить фокус на элемент IDD_PROPDLG в дереве ресурсов (в окне Resource View) и вызвать контекстное меню. Затем надо выбрать команду Resource Symbols. Появится диалог со списком всех идентификаторов, которые хранятся в файле resource.h. Не следует редактировать этот файл вручную.
Примечание 1
Примечание 1
Изменять числовые значения идентификаторов следует с большими предосторожностями, так как ошибки на этом этапе могут внести трудно распознаваемые отказы и нестабильную работу приложения. Надо сказать, что отслеживание корректности числовых значений идентификаторов всегда было слабым местом как Visual Studio, так и среды разработки Borland. Беру на себя смелость предположить, что уйма времени была затрачена разработчиками всех стран на поиск ошибок такого рода, так как сам потратил много усилий и времени пока не понял, что легче уничтожить ресурс и создать заново, чем пытаться найти новый диапазон числовых значений, который не затронет другие идентификаторы.
Если, несмотря на предостережения, вам захочется изменить числовое значение какого-либо идентификатора, то можете это сделать в окне Properties.
Редактор ресурсов может с возмущением отвергнуть ваш выбор. Тогда ищите другой диапазон с помощью уже рассмотренного диалога Resource Symbols. Эта тактика потенциально опасна. Повторюсь и скажу, что проще удалить и создать заново весь ресурс. Однако если вы самостоятельно выработаете или узнаете о более надежной технологии, то прошу сообщить мне. В этот момент следует запустить сервер и проверить наличие элементов на новой странице свойств. Если что-то не так, надо внимательно проверить, а возможно, и повторить все шаги создания вкладки.
Идентификаторы элементов управления
Таблица 9.2. Идентификаторы элементов управления
Для кнопки Quads установите свойство Group в положение True, а для кнопки Strips — в False. Обе они должны иметь свойство Auto в состоянии True. Важно еще то, что числовые значения их идентификаторов должны следовать по порядку. Для кнопки Data File установите свойство DefaultButton. Для выпадающего списка снимите свойство Sort (сделайте его False) и слегка растяните вниз его окно в открытом состоянии, для этого сначала нажмите кнопку раскрывания. Для ползунка вы можете установить свойство Point в положение Top/Left. Обратите внимание на тот факт, что в режиме дизайна вы можете открыть с помощью правой кнопки мыши диалог со страницами свойств для элемента IDC_OPENGL, одну из которых мы создавали в предыдущем проекте. Теперь с помощью Studio.Net введите в диалоговый класс обработчики следующих событий:
Элемент
Идентификатор
Диалог
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
Ниже мы приведем тела этих функций, а сейчас отметим, что все они пользуются услугами класса-оболочки для прямого вызова методов СОМ-сервера. Однако, как вы могли заключить из рассмотрения кодов класса COpenGL, на самом деле вызов будет происходить с помощью интерфейса IDispatch, а точнее его метода Invoke. Функция cwnd: : invokeHelper, вызов которой вы видите во всех методах COpenGL, преобразует параметры к типу VARIANTARG, а затем вызывает функцию Invoke. Если происходит отказ, то Invoke выбрасывает исключение.
В диалоговом классе мы попутно произвели упрощения, которые связаны с удалением ненужных функций OnPaint и OnQueryDragicon. Эти изменения обсуждались при разработке приложения Look. Во избежание недоразумений, которые могут возникнуть в связи с многочисленным ручным редактированием, приведем коды как декларации, так и реализации класса CTestGLDlg:
//=== Декларация диалогового класса (Файл TestGLDlg.h)
#include "opengl.h"
#pragma once
class CTestGLDlg : public CDialog
{
public:
CTestGLDlg(CWnd* p = NULL);
enum
{
IDD = IDD_TESTGL_DIALOG
};
//======= Объект класса-оболочки
COpenGL m_Ctrl;
//======= Запоминаем способ изображения
BOOL m_bQuads;
//======= Реакции на регуляторы в окне диалога
void OnSelchangeFill(void);
void OnClickedFilename(void);
afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
void OnCiickedBkcir(void);
void OnClickedQuads(void);
void OnClickedStrips(void);
protected:
virtual
void DoDataExchange(CDataExchange* pDX) ;
virtual BOOL OnlnitDialog();
afx_msg void OnSysCommand(UINT nID, LPARAM IParam);
DECLARE_MESSAGE_MAP()
};
В файл реализации методов класса мы кроме функций обработки сообщений от элементов управления вставили код начальной установки этих элементов. Для этой цели нам опять понадобилась связь с сервером, которую обеспечивает объект m_ctrl класса-оболочки. Характерным моментом является то, что обрабатываем событие WM_HSCROLL, которое поступает окну диалога, вместо того чтобы обработать уведомляющее событие NM_RELEASEDCAPTURE, которое идет от элемента типа Slider Control. Такая тактика позволяет реагировать на управление ползунком клавишами, а не только мышью:
#include "stdafx.h"
#include "TestGL.h"
#include "TestGLDlg.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = _FILE_;
#endif
//====== Пустое тело конструктора
CTestGLDlg::CTestGLDlg(CWnd* p) : CDialog(CTestGLDlg::IDD, p){}
void CTestGLDlg::DoDataExchange(CDataExchange* pDX) {
//====== Связывание переменной с элементом
DDX_Control(pDX, IDCJDPENGL, m_Ctrl);
CDialog::DoDataExchange(pDX);
}
//====== Здесь мы убрали ON_WM_PAINT и т. д.
BEGIN_MESSAGE_MAP(CTestGLDlg, CDialog) ON_WM_SYSCOMMAND()
//
}
}
AFX_MSG_MAP
ON_CBN_SELCHANGE(IDC_FILL, OnSelchangeFill)
ON_BN_CLICKED(IDC_FILENAME, OnClickedFilename)
ON_WM_HSCROLL()
ON_BN_CLICKED(IDC_BKCLR, OnClickedBkclr)
ON_BN_CLICKED(IDC_QUADS, OnClickedQuads)
ON_BN_CLICKED(IDC_STRIPS, OnClickedStrips)
END_MESSAGE_MAP()
//===== CTestGLDlg message handlers
BOOL CTestGLDlg::OnInitDialog()
{
//====== Добываем адрес меню управления окном
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu)
{
//====== Добавляем команду About
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING,
IDM_ABOUTBOX,"About...");
}
//====== Загружаем стандартный значок
HICON hMylcon = ::LoadIcon(0,(char*)IDI_WINLOGO);
Setlcon(hMylcon, TRUE); // Set big icon Setlcon(hMylcon, FALSE);
// Set small icon
CDialog::OnInitDialog();
//====== Начальная установка элементов
CComboBox *pBox = (CComboBox*)GetDlgltem(IDC_FILL);
pBox->AddString("Points"); pBox->AddString("Lines");
pBox->AddString("Fill"); pBox->SetCurSel (2);
//==== Выясняем состояние режима изображения полигонов
m_Ctrl.GetQuad(&m_bQuads);
WPARAM w = m_bQuads ? BST_CHECKED : BST_UNCHECKED;
//===== Устанавливаем состояние переключателя
GetDlgltem(IDC_QUADS)->SendMessage(BM_SETCHECK, w, 0);
w = m_bQuads ? BST_UNCHECKED : BST_CHECKED;
GetDlgltem(IDC_STRIPS)->SendMessage(BM_SETCHECK, w, 0);
return TRUE;
}
void CTestGLDlg::OnSysCommand(UINT nID, LPARAM iParam)
{
if ((nID S OxFFFO) == IDM_ABOUTBOX)
{
CDialog(IDD_ABOUTBOX).DoModal();
}
else
{
CDialog::OnSysCommand(nID, IParam);
}
}
//====== Выбор из списка типа Combo-box
void CTestGLDlg::OnSelchangeFill(void) "'*
{
DWORD sel = ((CComboBox*)GetDlgltem(IDC_FILL))->GetCurSel();
sel = sel==0 ? GL_POINT : sel==l ? GL_LINE
: GL_FILL;
m_Ctrl.SetFillMode(sel);
}
//==== Нажатие на кнопку запуска файлового диалога
void CTestGLDlg::OnClickedFilename(void)
{
m_Ctrl.ReadData();
}
//====== Реакция на сдвиг ползунка
void CTestGLDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
//====== Выясняем текущую позицию, которая не во
//====== всех случаях отражена в параметре nPos
nPos = ((CSliderCtrl*)GetDlgItem(IDC_XPOS))->GetPos() ;
m_Ctrl.SetLightParam (0, nPos);
}
//====== Запускаем стандартный диалог
void CTestGLDlg::OnClickedBkclr(void)
{
DWORD clr = m_Ctrl.GetFillColor() ;
CColorDialog dig (clr);
dig.m_cc.Flags |= CC_FULLOPEN;
if (dlg.DoModal()==IDOK)
{
m_Ctrl.SetFillColor(dlg.m_cc.rgbResult);
}
}
//====== Запоминаем текущее состояние и
//====== вызываем метод сервера
void CTestGLDlg::OnClickedQuads(void)
{
m_Ctrl.SetQuad(m_bQuads = TRUE);
}
void CTestGLDlg::OnClickedStrips(void)
{
m_Ctrl.SetQuad(m_bQuads = FALSE);
}
В настоящий момент вы можете запустить приложение, которое должно найти и запустить DLL-сервер ATLGL, генерирующий изображение по умолчанию и демонстрирующий его в окне внедренного элемента типа ActiveX. Сервер должен достаточно быстро реагировать на изменение регулировок органов управления клиентского приложения.
Подведем итог. В этом уроке мы научились:
Требования OpenGL
Требования OpenGL
Вместо тестового изображения с надписью ATL 4.0, которым мы научились кое-как управлять, поместим в окно СОМ-объекта OpenGL-изображение поверхности в трехмерном пространстве. Точнее, мы хотим дать клиенту нашего СОМ-объекта возможность пользоваться всей той функциональностью, которая была разработана в уроке 7. Вы помните, что изображение OpenGL может быть создано в окне, которое прошло специальную процедуру подготовки. Необходимо создать и сделать текущим контекст передачи OpenGL (HGRC). Вы также помните, что подготовку контекста надо рассматривать как некий обязательный ритуал, в котором порядок действий определен. Повторим его:
Чтобы использовать функции библиотеки OpenGL, надо подключить их к проекту. На этапе компоновки они будут интегрированы в коды СОМ-сервера.
#include
#include
#include
При работе с трехмерными координатами мы пользовались вспомогательным классом CPoint3D, который здесь нам тоже понадобится. Нужны будут и все переменные, которые были использованы ранее для управления сценой OpenGL. Там, если вы помните, был контейнер STL типа vector для хранения точек изображения. Использование контейнеров требует подключения соответствующих файлов заголовков, поэтому вставьте в конец файла stdafx.h следующие строки:
#include
Так как мы собираемся демонстрировать в окне OpenGL графики функций, диапазон изменения которых нам заранее не известен, то следует использовать предварительное масштабирование координат точек графика. Нам надо знать габариты изображаемого объекта и для упрощения этой задачи введем вспомогательную глобальную функцию корректировки экстремумов:
inline void MinMax (float d, floats Min, floats Max)
{
if (d > Max) Max = d;
else if (d < Min)
Min = d;
}
Описатель inline сообщает компилятору, что функцию можно не реализовывать в виде отдельной процедуры, а ее тело желательно вставлять в точки вызова, с тем чтобы убрать код обращения к стеку. Окончательное решение при этом остается за компилятором.
Трехмерная графика в проекте ATL
Трехмерная графика в проекте 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 удобно реализовать с помощью отдельного метода класса:
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 ();
}
Управление с помощью объекта классаоболочки
Управление с помощью объекта класса-оболочки
Для управления внедренным элементом ActiveX надо ввести в существующий диалоговый класс CTestGLDlg объект (переменную типа) класса-оболочки. Этот шаг тоже автоматизирован в Studio.Net, так как введение объекта влечет сразу несколько строк изменения кода.
Результатом работы мастера являются следующие строки программы:
Для обеспечения видимости вставьте в начало файла TestGLDlg.h директиву:
#include "opengl.h"
В конец файла Stdafx.h вставьте директивы подключения заголовков библиотеки OpenGL:
#include
// Будем пользоваться OpenGL
#include
Теперь следует поместить в окно диалога элементы управления. Здесь мы не будем пользоваться страницами свойств элемента, созданными нами в рамках предыдущего проекта. Вместо этого мы покажем, как можно управлять внедренным элементом ActiveX с помощью объекта m_ctrl. Перейдите в окно диалогового редактора и придайте окну диалога IDD_TESTGL_DIALOG.
Идентификаторы для элементов управления можно задать так, как показано в табл. 9.2.
Установка освещения
Установка освещения
Параметры освещения будут изменяться с помощью регуляторов, которые мы разместим на новой странице блока 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
//======= Заполняем транспортный массив 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, либо с помощью страницы свойств, которую мы вскоре создадим. В любом случае, эти методы должны либо получить параметр настройки изображения и перерисовать его с учетом настройки, либо вернуть текущее состояние запрашиваемого параметра настройки:
Вид новой вставки в блоке страниц свойств элемента ActiveX
Рисунок 9.2. Вид новой вставки в блоке страниц свойств элемента ActiveX
На рисунке показано окно диалога в активном состоянии, но вам еще предстоит поработать, чтобы довести его до этого вида. Здесь очень важно не торопиться и быть внимательным. Опыт преподавания в MS Authorized Educational Center (www.Avalon.ru) подтверждает, что большая часть ошибок вносится на стадии работы с ресурсами. Визуальные редакторы расслабляют внимание, и ошибки появляются там, где вы меньше всего их ждете.
В основных чертах окно имеет тот же облик, что и окно диалога по управлению освещением сцены, разработанное ранее (в MFC проекте). Но здесь есть два новых элемента, функциональность которых ранее была спрятана в командах меню. Так как в рамках этого проекта мы не имеем меню, то нам пришлось использовать элементы управления, сосредоточенные в нижней части окна диалоговой вставки. Во-первых, не забудьте, что справа от каждого ползунка вы должны расположить элемент типа static Text, в окне которого будет отражено текущее положение движка в числовой форме.
Кнопка Выбор файла, как и ранее, позволяет пользователю открыть файловый диалог и выбрать файл с данными для нового изображения. Выпадающий список Заполнение позволяет выбрать режим изображения полигонов (GL_FILL, GL_POINT или GL_LINE), а кнопка Quads/Strip изменяет режим использования примитивов при создании поверхности. Идентификаторы элементов управления мы сведем в табл. 9.1.
Внедрение элемента ActiveX в окно диалогового приложения
Рисунок 9.3. Внедрение элемента ActiveX в окно диалогового приложения
В отличие от Visual Studio б в конце этой процедуры в состав проекта (по умолчанию) не будет включен новый класс-оболочка (wrapper class) под именем CGpenGL. Такой класс необходим для дальнейшей работы с внедренным элементом ActiveX.
В документации бета-версии Studio.Net я нашел лишь намек на то, что wrapper-класс может быть создан с помощью ClassWizard. Однако мне не удалось добиться этого. Поэтому мы создадим класс-оболочку вручную. Конечно, здесь я использую заготовку класса, полученную в рамках Visual Studio 6. Она оказалась вполне работоспособной и в новой Studio.Net. Будем надеяться, что в следующих версиях Studio.Net рассмотренный процесс автоматического создания класса будет достаточно прозрачен.
Введение методов в интерфейс IOpenGL
Введение методов в интерфейс IOpenGL
На этом этапе важно решить, какие данные (свойства) и методы класса будут экспонироваться СОМ-объектом, а какие останутся в качестве служебных, для внутреннего пользования. Те методы и свойства, которые будут экспонированы, должны быть соответствующим образом отражены в IDL-файле. Те, которые нужны только нам, останутся внутри сервера. Для примера введем в число экспонируемых методов функцию GetLightParams, которая определяет действующие параметры освещения.
Проанализируйте изменения, которые появились в 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. Некоторые детали этого процесса обсуждались в предыдущем уроке.
Взаимодействие классов
Взаимодействие классов
Класс CPropDlg должен обеспечить реакцию на изменение регулировок, а класс COpenGL должен учесть новые установки и перерисовать изображение. Общение классов, как мы уже отметили, происходит по законам СОМ, то есть с помощью указателя на интерфейс. Здесь нам на помощь приходит шаблон классов CComQiPtr. Литеры «QI» в имени шаблона означают Querylnterface, что обещает нам автоматизацию в реализации запроса указателя на этот интерфейс. В классе переопределены операции выбора (->), взятия адреса (&), разадресации (*) и некоторые другие, которые упрощают использование указателей на различные интерфейсы. При создании объекта класса CComQiPtr, например:
CComQIPtr
он настраивается на нужный нам интерфейс, и далее мы работаем с удобствами, не думая о функциях Querylnterface, AddRef и Release. При выходе из области действия объекта р класса CGomQiPtr
Для обмена с окном диалоговой вставки введите в 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
//=== Пытаемся связаться с классом 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
//====== Класс для работы с 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, то есть цепочку вызовов функций. Для этого:
Этот опыт иллюстрирует тот факт, что все необычно в мире 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
//====== Устанавливаем конкретный параметр
if FAILED (p->SetLightParam (num, m_Pos [num] ) )
ShowError();
return 0;
}
}
return 0;
}
В данный момент вы можете проверить функционирование регуляторов в суровых условиях СОМ. Они должны работать.
Теперь введем реакцию на выбор пользователем новой строки в окне выпадающего списка. Для этого выполните следующие действия:
LRESULT CPropDlg
::OnSelchangeFillmode(WORD/*wNotifyCode*/, WORD /*wID*/,
HWND hWndCtl, BOOL& bHandled)
{
//====== Цикл пробега по всем объектам типа PropDlg
for (UINT i = 0; i < m_nObjects; i++)
{
CComQIPtr
// Выясняем индекс строки, выбранной в окне списка
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 и в ячейке справа выберите действие
LRESULT CPropDlg::OnClickedQuads(WORD /*wNotifyCode*/,
WORD /*wID*/, HWND /*hWndCtl*/, BOOL& bHandled)
{
//====== По всем объектам PropDlg
for (UINT i = 0; i < m_nObjects; i++)
{
//====== Добываем интерфейсный указатель
CComQIPtr
//====== Переключаем режим
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
//====== Вызываем функцию класса 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.
Из жизни студентов
Ассоциативные контейнеры
Ассоциативные контейнеры
К ассоциативным контейнерам принадлежат: set, multiset, hash set, hash multiset, map, multimap, hash_map, hash_multimap. Они поддерживают эффективный поиск значений (values), связанных с ключом (key). Они позволяют вставить и удалить элемент, но в отличие от последовательностей не позволяют вставить элемент в заранее определенную и указанную позицию. Различают сортированные ассоциативные контейнеры (set, multiset, map, multimap) и хешированные (hashed) ассоциативные контейнеры (hash_set, hash_multiset, hash_map, hash_ / multimap).
Сортированные контейнеры соблюдают отношение порядка (ordering relation) для своих ключей, причем два ключа считаются эквивалентными, если ни один из них не меньше другого. Например, если отношение порядка не учитывает регистр, то ключ "strict rules" эквивалентен ключу "Strict Rules". Сортированные контейнеры хороши тем, что они гарантируют логарифмическую эффективность (complexity) большинства своих операций. Это гораздо более сильная гарантия, чем та, которую предоставляют хешированные ассоциативные контейнеры. Последние гарантируют постоянную эффективность только в среднем, а в худшем случае — линейную.
Хешированные ассоциативные контейнеры основаны на той или иной реализации хэш-таблиц (см. монографию Кнут Д. Искусство программирования, т. 3, Сортировка и поиск, 1999). Элементы в таком контейнере не упорядочены, хотя их можно добывать последовательно. Если вы вставите или удалите элемент, то последовательность оставшихся элементов может измениться, то есть она не гарантируется. Преимуществом рассматриваемого типа контейнеров является то, что в среднем они значительно быстрее сортированных ассоциативных контейнеров. Удачно подобранная функция хеширования позволяет выполнять вставки, удаления и поиск за постоянное, не зависящее от п, время. Кроме того, она обеспечивает равномерное распределение хешированных значений и минимизирует количество коллизий.
Примечание 1
Примечание 1
Поясним кратко суть хеширования. Она состоит в том, что каждому ключу (key) ставится в соответствие значение (value). Например, ключом могло бы быть имя абонента — строка символов, а значением — номер его телефона. Поиск значения по ключу осуществляется с помощью хеш-таблицы (hash table), которая ассоциирует ключевой объект с объектом типа значение. Эффективность работы зависит от алгоритма хеширования, который преобразует ключ в число из какого-то диапазона. Это число еще не value, а скорее индекс для выбора значения (value). При этом возможна ситуация коллизии (collision), когда два разных ключа будут преобразованы в одно и то же число. В таких случаях производится обработка коллизии по специальному алгоритму. Обычно используются списки для хранения ключей, случайно попавших в коллизию, или, как говорят, в одно ведро (bucket). Списки — это потеря эффективности поиска, но хорошие алгоритмы хеширования гарантируют очень низкую вероятность коллизий.
Если ключи не уникальны, то можно выбрать не hаsh_mар-контейнер, а контейнер типа hash_multimap. Если нужно просто хранить множество каких-то объектов, например строк текста, не ассоциируя их с другими объектами, то стоит подумать о контейнере типа hash_set. Ну а в случае, если среди этих объектов могут попасться одинаковые, то выбором может стать контейнер типа hash_multiset.
Хешируемые типы контейнеров все-таки упорядочивают контролируемую ими последовательность, вызывая встроенный в него объект hash Traits класса value_compare. Вы имеете доступ к этому объекту с помощью метода key_comp. В общем случае два элемента признаются либо одинаковыми, либо какой-либо из них признается меньшим. Но реальная упорядоченность элементов зависит от функции хеширования, функции, задающей отношение порядка и текущего размера хэш-таблицы, поэтому в общем случае невозможно предсказать порядок следования элементов. Важной характеристикой ассоциативных контейнеров является то, что вставка элементов не портит итераторы, а удаление делает недействительными только те итераторы, которые указывают на удаляемый элемент.
с приоритетами тоже является адаптером,
Очередь с приоритетами тоже является адаптером, который позволяет вставку элементов, инспекцию и удаление верхнего (top) элемента. Она не допускает итераций прохода по своим элементам. Ее характерным отличием является то, что верхний элемент является самым большим в том смысле, в котором в шаблоне используется функциональный объект (Compare — сравнение объектов). Для разнообразия приведем объявление шаблона:
template
<
class Type,
class Container=vector
class Compare=less
>
Отсюда видно, что по умолчанию очередь с приоритетами основана на контейнере типа vector и для сравнения приоритетов она использует предикат lesso. Для объектов класса Man — это внешняя friend-функция operator< (), которая упорядочивает последовательность по возрасту. Но очередь с приоритетами должна расставить элементы по убыванию приоритетов. Проверим это утверждение с помощью следующего фрагмента:
void main () {
//===== Priority queue (by age)
priority_queue
men.push (zoran);
//== Для проверки поведения вставляем объект повторно
men.push (zoran);
men.push (joy);
men.push (mela); men.push (win);
cout«"priority_queue size: "«men. size () «endl;
int i=0;
while ('men.empty())
{
cout « "\n"« ++i«". "«men.top();
men.pop();
}
}
Выходом этой программы будет такой текст:
priority_queue size: 5
1. Winton Kelly, Age: 50
2. Zoran Todorovitch, Age: 27
3. Zoran Todorovitch, Age: 27
4. Joy Amore, Age: 18
5. Melissa Robinson, Age: 9
Как видно, объекты выстроены по убыванию возраста. Очереди и стеки допускают повторение элементов.
Использование STL
Использование STL
В подобных ситуациях владение стандартными динамическими структурами данных и алгоритмами может сэкономить массу усилий, так как их разработчики уже выполнили большую часть неблагодарной черновой работы, тщательно отладили динамику жизни структур данных и все ветви алгоритмов. Кроме того, они провели анализ эффективности алгоритмов и привели их оценки. Сравним для примера две реализации алгоритма сортировки. Все знают, что рекурсивный алгоритм быстрой сортировки Quicksort, —изобретенный С. A. R. Ноаге в 1960 году, считается одним из самых эффективных в смысле количества необходимых операций для выполнения работы. Так, для сортировки массива в п элементов этому алгоритму понадобится всего лишь O(n Iog2 n) операций.
В библиотеке, подключаемой файлом заголовков stdlib.h, есть функция qsort, которая использует алгоритм Quicksort для сортировки массива элементов произвольного типа. Кроме сортируемого массива в функцию qsort необходимо передать адрес функции, которая сравнивает два элемента между собой. Алгоритм использует это сравнение для упорядочивания массива. Следующая программа демонстрирует, как можно воспользоваться функцией qsort для сортировки массива целых, вводимого пользователем. Для ее отладки я воспользовался проектом Console консольного типа, процедура создания которого была описана ранее. Из-за ошибок, связанных с использованием бета-версии Studio.Net, мне пришлось изменить конфигурацию проекта с Debug на Release. Это можно сделать, дав команду Build > Configuration Manager и выбрав Release в окне Active Solution Configuration:
#include
#include
using namespace std;
//=== Внешняя функция сравнения переменных типа int
inline int crop (const void *a, const void *b)
{
int i = *(int *)a, j = *(int *)b;
return (i < j) ? -1 : (i > j) ? 1 : 0;
}
void main()
{
int array [1024],
// Сортируемый массив n - 0;
// Счетчик элементов
cout «"Enter some integers (Press Ctrl+z to stop)\n";
//=== Вводим по принципу "пока не надоест". Для выхода
//=== из цикла надо ввести EOF (то есть Ctrl+z, Enter)
while (cin » array[n++])
//==== Шаг назад, так как мы сосчитали EOF n—;
qsort (array, n, sizeof(int), cmp) ;
for (int i = 0; i < n; i++)
cout « array[i] « endl;
cout « endl;
}
Теперь сравним этот фрагмент с тем, который использует стандартный контейнер vector и алгоритм sort из библиотеки STL (Standard Template Library):
#include
#include
#include
using namespace std;
void main ()
{
vector
int i; // Рабочая переменная
cout «"Enter some integers (Press Ctrl+z to stop)\n";
while (cin » i) // Вводим те же числа
v.push_back (i); // Помещаем в конец контейнера
//======= Сортируем контейнер, используя тип
//======= упорядочения, принятый по умолчанию
sort (v.begin () , v.end());
for (i =0; i < int(v.size()); i++)
cout « v[i] « endl;
cout « endl;
}
По умолчанию алгоритм sort использует для сравнения элементов операцию меньше, то есть сортирует контейнер по возрастанию. Сравнительная оценка эффективности двух реализаций, которую проводили специалисты (числа, конечно, вводились не вручную), показывает, что эффективность второй версии выше в 10-20 раз. Она зависит от размера массива и степени его упорядоченности. Приведем одну из причин такого результата.
Важно помнить, что рекурсия сама по себе стоит дорого, поэтому важны детали реализации конкретного алгоритма. Над деталями реализации алгоритмов библиотеки STL потрудились специалисты, и результатом их труда является достаточно высокая эффективность, которую отмечают многие разработчики. К сожалению, возможности STL очень скудно описаны в MSDN, хотя в мире существуют книги, где библиотека и технология ее использования для решения конкретных задач описаны достаточно подробно. Среди доступных нам книг на русском языке, конечно, следует отметить последнюю книгу Б. Страуструпа «Язык программирования C++», 3-е изд. — СПб: «Невский Диалект», 1999. Но она описывает библиотеку концептуально. В ней почти нет текстов программ, готовых к употреблению в среде Visual Studio. Поэтому мне захотелось дать быстрый путь к овладению некоторыми возможностями библиотеки тем читателям, которые обладают хорошим алгоритмическим мышлением, имеют некоторый опыт работы с динамическими структурами данных, но не знакомы с особенностями структуры и использования STL. Ниже будут приведены примеры практического использования контейнеров и алгоритмов STL, но не будет подробного описания заложенных в них принципов.
Из жизни студентов
Из жизни студентов
Как показывает практика, студенты по-разному относятся к тому факту, что доля курсовых проектов, которые необходимо выполнять в виде компьютерных приложений, непрерывно растет. Некоторые их очень любят, так как подобные проекты позволяют продемонстрировать неординарность мышления, изобретательность и свой собственный «неподражаемый» стиль программирования, другие ненавидят, так как работающее приложение невозможно создать без тщательной проработки почти всех деталей, в том числе и тех, которые кажутся мелкими и незначительными. Сначала компилятор языка, а затем и операционная система хладнокровно бракуют малейшую неточность, непоследовательность, недоговоренность и пренебрежение деталями. В устном докладе и даже в письменном отчете можно скрыть или завуалировать перечисленные дефекты, но компьютерный проект обнажит их, продемонстрирует со всей очевидностью, а зачастую и усилит.
Контейнер типа set
Контейнер типа set
Множество (set) является ассоциативным контейнером, который хранит объекты типа key. В этом случае говорят о типе Simple Associative Container, имея в виду, что как value, так и key имеют один тип key. Говорят также о Unique Associative Container, имея в виду, что в контейнере типа set не может быть одинаковых элементов. Рассмотрим работу контейнера на примерах. Не забудьте вставить директиву #include
void main ()
{
//======== Создаем множество целых
set
s.insert(1);
s.insert(2);
s.insert (3);
//======= Повторно вставляем единицу (она не пройдет)
s.insert (1);
//==== Два раза вставляем "в конец последовательности"
s. insert (--s.end() , 4); s.insert(—s.endO, -1);
pr(s,"Set of ints");
//======== Второе множество
set
for (int i=l; i<5; i++) ss.insert (i*10);
//======== Вставляем диапазон
s. insert (++ss. begin () , —s s.end() );
pr(s, "After insertion"); cout«"\n\n";
}
Эта программа выведет в окно Output следующие строки:
Set of ints # Sequence:
1. -1
2. 1
3. 2
4. 3
5. 4
After insertion # Sequence:
1. -1
2. 1
3. 2
4. 3
5. 4
6. 20
7. 30
Как видно из распечатки, несмотря на то что и 4 и -1 были вставлены в конец последовательности, контейнер сам распорядился порядком следования и разместил элементы в порядке возрастания ключей. Вставка диапазона из другого множества также происходит по этим законам. Следующий содержательный пример я обнаружил на сайте компании Silicon Graphics. Он приведен в слегка измененном виде:
//========= Предикат
inline bool NoCase(char a, char b)
{
// Определяет отношение less для обычных символов
// без учета регистра (Подключите stdlib.h)
return tolower(a) < tolower (b) ; !;
}
//========= Функциональный объект
struct LessStr
{
//==== Определяет отношение less для C-style строк
bool operator()(const char* a, const char* b) const
{
return strcmp(a, b) < 0;
}
};
Два отношения порядка для типов данных, хорошо вам знакомых (char и char*), для разнообразия заданы: одно в виде предиката, другое в виде функционального объекта. Ниже они будут использованы в конструкторе шаблона классов set. Тем самым определен порядок сортировки элементов контейнера:
void main ()
{
//====== Обычные неупорядоченные массивы символов
const int N = 6; const char* a[N] =
{
"Set", "Pet", "Net", "Get", "Bet", "Let"
};
const char* b[N] =
{
"Met", "Wet", "Jet",
"Set", "Pet", "Net",
} ;
//======== Создаем два множества обычных строк,
//======== определяя отношение порядка
set
set
//======== Создаем пустое множество
set
//======== Выходной итератор привязываем к cout
cout « "Set A: {";
copy (A.begin (), A.end.(),
ostream_iterator
cout « ' } ' ;
cout « "\n\nSet B:
copy (B.begin (), B.end(), .. ostream_iterator
//======= Создаем и выводим объединение двух множеств
cout « "\n\nUnion A U В: ";
set_union (A.begin () , A.end(), B.begin(), B.end(),
ostream_iterator
LessStr () )';
//======= Создаем и выводим пересечение двух множеств
cout « "\n\nlntersection А & В: ";
set_intersection (A.begin () , A.end(), B.beginO, B.end(), ostream_iterator
//===== Создаем разность двух множеств
//===== Используем inserter для заполнения множества С
set_dif ference (A.begin () , A.end(), B.beginO, B.end(),
inserter (С, C.begin()),
LessStr() ) ;
cout « "\n\nDifference A/B: ";
//===== Копируем множество прямо в выходной поток сору
С. begin () , С.
end ();
ostream_iterator
С. clear () ;
//===== Повторяем в обратную сторону
set_dif ference (В. begin () , B.endO, A.begin(), A.end(),
inserter (С, C.begin()), LessStrO);
cout « "\n\nDifference B/A: ";
copy (C.begin (), C.end(),
ostream_iterator
cout « "\n\n";
//====== Выводим разделитель
vector
ostream_iterator
copy (line .begin О , line.endO, os) ;
//====== Обычные массивы символов
char D[] = { 'a', 'b', 'с', 'd', ' e', 'f' };
char E[] = { 'A', 'B', 'C1, 'G', 'H1, 'H' };
cout « "\n\nSet D: ";
for (int i=0; i
cout « "\n\nSet E: ";
for (int i=0; i
cout « E[i]«",";
cout « "\n\nSymmetric Difference D/E (nocase): ";
//====== Используем алгоритм set_symmetric_difference
//====== для обычных массивов символов
set_symmetric_difference(D, D + N, E, E + N,
ostream_iterator
cout«"\n\n"; }
Новые возможности STL, которые использованы в этом фрагменте, — это использование адаптера insert_iterator и копирование содержимого контейнера прямо в выходной поток (см. ostream_iterator). Вывод в поток осуществляется с помощью особого типа итератора ostream_iterator, который осуществляет форматируемый вывод объектов типа Т в указанный выходной поток (ostream). Шаблон класса ostream_iterator настраивается на тип данных, в нашем случае const char*, а затем char, и берет в качестве параметров объект потокового вывода (cout) и разделитель, который мы специально изменяем по ходу дела для того, чтобы вы его обнаружили.
Insert_iterator — это адаптер (настройщик) итератора, который настраивает операнд, являющийся мишенью операции. Присвоение с помощью (сквозь призму) такого итератора вставляет объект в контейнер, раздвигая его, то есть, используя метод insert. Он следит за текущей позицией в контейнере (insertion point) и производит вставку перед ней. Здесь вы, однако, должны учесть различную семантику операции вставки в различные типы контейнеров. Если это последовательность (sequence), то все происходит именно так, как только что было сказано, но если это сортируемый ассоциативный контейнер, то вы не можете управлять позицией вставки. Для таких контейнеров указание места вставки служит лишь отправной точкой для поиска реальной позиции в контейнере. В результате вставки при выводе вы увидите упорядоченную по возрастанию ключа последовательность. Порядок вставки в сортируемые ассоциативные контейнеры не влияет на порядок элементов, который вы увидите на экране. Однако он может повлиять на эффективность процесса вставки.
Парой называется шаблон класса, который хранит пару объектов различного типа. Пара похожа на контейнер в том, что она владеет своими элементами, но она не является контейнером, так как не поддерживает стандартных методов и итераторов, присущих контейнерам. У пары есть два элемента данных (first, second), которые дают возможность манипулировать вложенными объектами. Следующий фрагмент иллюстрирует логику использования пары:
pair
if (result.first)
print(result.second);
else
report_error() ;
Ассоциативные контейнеры используют тип пар _Pairib. Он определяет пару:
pair
Первый элемент каждой такой пары является итератором соответствующего типа, а второй — результатом какого-либо действия. Например, метод insert возвращает пару типа _Pairib, анализируя которую вы можете узнать результат вставки (успех или неудача). Рассмотрим пример:
void main ()
{
//========== Массив объектов класса Man
Man ar[] =
{
joy,duke, win, joy,charlie
);
uint size = sizeof(ar)/sizeof(Man);
//========== Создаем множество объектов класса Man
set
//========== Ищем объект и удаляем его
set
if (p != s.end() )
{
s.erase(p);
cout « "\n\n"« joy «" found and erased";
}
pr(s,"After erasure");
//========== Объявляем пару
set
//========== Пробуем вставить объект
pib = s.insert(joy);
//========== Анализируем результат вставки
cout « "\n\nlnserting: " « *pib.first « "\nResult is: " « pib.second;
//========== Пробуем вставить повторно
pib = s.insert(joy);
cout « "\n\nlnserting: " « *pib.first « "\nResult is: " « pib.second;
//========== Сравниваем ключи
cout « "\n\ns.key_comp() (zoran,count) returned "
« s.key_comp()(zoran,ar[0]);
cout « "\n\ns.key_comp()(count,zoran) returned "
« s.key_comp()(ar[0],zoran);
cout <<"\n\n";
}
Приведем результат работы этой программы:
Set of Man # Sequence:
1. Joy Amore, Age: 18
2. Winton Kelly, Age: 50
3. Charlie Parker, Age: 60
4. Duke Ellington, Age: 90
Joy Amore, Age: 18 found and erased After erasure # Sequence:
1. Winton Kelly, Age: 50
2. Charlie Parker, Age: 60
3. Duke Ellington, Age: 90
Inserting: Joy Amore, Age: 18
Result is: 1
Inserting: Joy Amore, Age: 18
Result is: 0
s.key_comp()(zoran,count) returned 0 s.key_comp()(count,zoran) returned 1
Контейнеры библиотеки STL
Контейнеры библиотеки STL
Теперь, когда вы вспомнили, что такое шаблоны функций и шаблоны классов, мы можем исследовать возможности стандартной библиотеки шаблонов STL. В июле 1994 года специальный комитет Международной организации по принятию стандартов (ANSI/ISO C++) проголосовал за то, чтобы принять STL в качестве части стандарта языка C++. Предложение было основано на исследовании обобщенного (generic) программирования и концепции библиотеки (generic software library), которое проводили Alex Stepanov, Meng Lee и David Musser. Главной целью при разработке библиотеки было достижение общности (generality) подхода к различным структурам данных и алгоритмам их обработки без ущерба эффективности кода.
В STL определены два типа контейнеров — последовательности (sequence containers) и ассоциативные контейнеры. Все контейнеры предназначены для хранения данных любого типа. Последовательности предполагают последовательный доступ к своим элементам, а ассоциативные контейнеры работают по принципу ассоциации ключа (key) с его значением (value). Можно считать, что ассоциативные контейнеры хранят пары произвольных элементов и производят поиск по ключу, используя технику hash-таблиц. В STL существует три типа последовательных контейнеров: vector, deque И list.
Контейнеры типа hash_multimap
Контейнеры типа hash_multimap
Хешированный ассоциативный контейнер типа hash_multimap основан на встроенной реализации хэш-таблиц. Вы помните, что преимуществом такого типа контейнеров является быстродействие, которое в среднем значительно выше, чем у сортированных ассоциативных контейнеров. Упорядоченность элементов в таком контейнере не гарантируется, но вы можете по определенной системе добывать их С ПОМОЩЬЮ метода hash_multimap: :equal_range.
Предположим, что ваша база данных содержит сведения о сотрудниках — объектах класса Man, многих отделов какой-то организации. В примере мы возьмем только два отдела (100 и 115). Так как мы хотим быстро получать информацию о сотрудниках, то выбираем в качестве структуры для хранения данных в памяти хешированный ассоциативный контейнер. Очевидно, что если в качестве ключевого поля для него выбрать номер отдела, то поле не будет уникальным. Этот факт окончательно определяет выбор типа контейнера— hash_multimap.
Вы также, вероятно, помните, что все контейнеры типа тар — это Pair Associative контейнеры, так как они хранят пары типа pair
//======= ManPair - это тип используемых пар
typedef pair
//======= ManMap - это тип контейнера
typedef hash_multimap
//======= ManMapIt — это тип итератора
typedef ManMap::const_iterator ManMapIt;
Отметьте, что мы выбрали самый простой способ определения контейнера. Более точным описанием, которое намекает вам на возможность усложнения структуры, будет:
typedef hash_multimap
hash_compare
Отсюда ясно, что можно изменить предикат, по которому производится сравнение элементов контейнера. Для выбора всех сотрудников определенного отдела мы собираемся использовать метод:
equal_range(int /*Номер отдела*/);
который возвращает пару итераторов. Первый итератор пары указывает на начало диапазона внутри контейнера из сотрудников указанного отдела, а второй — на конец этого диапазона. Теперь пора в бой. Надо писать код, реализующий работу контейнера.
void main( )
{
typedef pair
typedef hash_multimap
typedef ManMap::const_iterator ManMapIt;
//====== Создаем пустой контейнер типа hash_multimap ManMap h;
//====== Наполняем его сотрудниками
h.insert (ManPair (100, тагу));
h.insert (ManPair (115, joe));
h.insert (ManPair (100, win));
h.insert (ManPair (100, charlie));
h.insert (ManPair (115, liza));
h.insert TManPair (115, joy));
//====== При выводе пользуемся парой
cout « "Contents of Hash Multimap\n\n";
for (ManMapIt p = h.begin();
p != h.end(); p++)
cout « "\n" « p->first
«". " « p->second;
//====== Выбираем диапазон (сотрудники 100-го отдела)
pair
//====== Вновь пользуемся парой
cout « "\n\nEmployees of 100 department\n\n";
for (p = pp.first; p != pp.second; ++p)
cout « "\n" « p->first
«"." « p->second; cout « "\n\n";
}
He лишнее напомнить, что приведенный код надо дополнить объявлениями объектов класса Man и вставкой директивы #include
Contents of Hash Multimap
115. Liza Dale, Age: 17
115. Joy Amore, Age: 18
115. Joe Doe, Age: 30
100. Winton Kelly, Age: 50
100. Charlie Parker, Age: 60
100. Mary Poppins, Age: 36
Employees of 100 department
100. Winton Kelly, Age: 50
100. Charlie Parker, Age: 60
100. Mary Poppins, Age: 36
Контейнеры типа map
Контейнеры типа map
Отображение (map) является сортируемым ассоциативным контейнером, который ассоциирует объекты типа key с объектами типа value. Map — это Pair Associative Container, так как он хранит пары типа pair
map
map
//========= Создаем новый тип для удобства
typedef pair
//========= Изменяем компоненты пары
p.first = "Tusday";
p.second = 2;
pib = m.insert(p);
cout « "\n\nlnserting: "
« (*pib.first).first « ","
« (*pib.first).second
« "\nResult is: " « pib.second;
pib = m.insert(p);
cout « "\n\nlnserting: "
« (*pib.first).first « ","
« (*pib.first).second
« "\nResult is: " « pib.second;
//========= Работаем с индексом
m["Wednesday"] = 3;
m["Thirsday"] = 4;
m["Friday"] = 5;
//========= Работаем с динамической памятью
MyPair *pp = new MyPair("Saturday", 6);
m.iftsert(*pp);
delete pp;
cout«"\n\n\t
for (it = m.begin ();
if != m.end(); it++)
cout « "\n(" « it->first«","<
cout«"\n\n";
}
Результат работы этого фрагмента выглядит так:
Inserting: Tusday, 2 Result is: 1
Inserting: Tusday, 2 Result is: 0
(Friday, 5) (Monday, 1) (Saturday, 6) (Thirsday, 4) (Tusday, 2) (Wednesday, 3)
Как видите, пары отсортированы в лексикографическом порядке. Если потребуется восстановить естественный порядок, то это можно сделать, поменяв порядок следования аргументов при объявлении шаблона на
map
Такую замену придется сделать и для всех других, связанных с шаблоном типов данных. Отметьте также, что при работе с отображениями недостаточно разадресо-вать итератор (*it), чтобы получить объект им указываемый. Теперь вы должны писать (*it) .first или it->first, чтобы получить какой-то объект. Характерно, что эти выражения могут стоять как в левой, так и в правой части операции присвоения, то есть вы можете записать:
it->first = "Sunday";
int n = it->second;
Контейнеры типа queue
Контейнеры типа queue
Очередь — это тоже,адаптер, который предоставляет ограниченное подмножество функциональности контейнера. Говорят, что очередь — это структура данных с дисциплиной доступа "first in first out" (FIFO). Элементы, вставляемые в конец очереди, могут быть выбраны спереди. Это означает, что метод queue:: front () возвращает самый «старый» элемент, то есть тот, который был вставлен в очередь least recently — первым из тех, что еще живы. Очередь, так же как и стек, не допускает итераций прохода по своим элементам. По умолчанию она основана на контейнере типа deque. Сравнение стека и очереди приведены в следующем фрагменте (Подключите
void main ()
{
//========== Массив объектов класса Man
Man ar[] =
{
joy, mаrу, win
};
uint size = sizeof(ar)/sizeof(Man);
//========== Создаем с.тек объектов класса Man
stack
for (uint i=0; i
cout « "Stack of Man:\n\n";
while (s.size ())
{
cout « s.top() « "; ";
s.pop ();
}
//========== Создаем очередь объектов класса Man
queue
for (i=0; Ksize; i++) q.push(ar[i]);
cout « "\n\nQueue of Man:\n\n";
while (q.size ())
{
cout « q.front() « "; ";
q.pop(); }
cout«"\n\n";
}
Поиск с помощью предиката
Поиск с помощью предиката
Поиск первого объекта, который удовлетворяет условию, заданному предикатом, осуществляется с помощью шаблона функции f ind_if. В качестве третьего, параметра она требует задать имя функции-предиката. Введите в состав класса объявление такой функции:
//========= Предикат принадлежности к teenager
friend bool Teen (Man& m);
Тело этой функции определите глобально, то есть вне класса:
//========= Предикат принадлежности к teenager
bool Teen(Man& m)
{
return 13 < m.m_Age && m.m_Age < 19;
}
Теперь покажем, как искать в контейнере первый элемент, удовлетворяющий предикату, а также все элементы, удовлетворяющие этому условию. Ниже нам понадобятся несколько объектов класса Man, поэтому мы ввели их объявление в начало функции main. Далее везде мы будем считать, что эти объекты присутствуют в функции main, но не будем приводить их заново:
void main ()
{
//======== Набор объектов класса Man
Man joe("Joe Doe",30),
joy ("Joy Amore", 18) ,
Mаrу("Mary Poppins",36),
duke("Duke Ellington",90),
liza("Liza Dale", 17),
simon("Simon Paul",15),
zoran("Zoran Todorovitch",27) ,
Charlie("Charlie Parker",60),
win("Winton Kelly",50),
mela("Melissa Robinson",9);
vector
men.push_back (zoran);
men.push_back (liza);
men.push_back (simon);
men.push_back (mela);
// Поиск первого объекта, удовлетворяющего предикату
vector
find_if (men .begin () ,
men.endO, Teen);
//======== Ручной поиск всех таких объектов
while (p != men.end())
{
cout « "\nTeen: " « *p;
p = find_if(++p, men.endO, Teen);
}
cout « "\nNo more Teens\n";
//======== Подсчет всех teenagers
uint teen = count_if (men.begin (),men.endO , Teen);
cout « "\n\n Teen totals: " « teen;
//======== Выполняем функцию для всех объектов
for_each(men.begin(),men.end(),OutTeen) ;
//======== Используем обратный итератор
cout«"\n\nMan in reverse\n";
for (vector
r = men.rbegin();
r != men.rendO; r++) cout«*r«";
//======== Заполняем вектор целых
vector
for (int i=l; i<4; i++) v.push_back(i);
//======== Иллюстрируем алгоритм и адаптивный functor
transform(v.begin () , v.end(), v.begin (), negate
pr(v,"Integer Negation");
//======== Создаем еще два вектора целых
vector
//======== Иллюстрируем алгоритм заполнения вектора
fill (vl.begin (), vl.endO, 100);
//======== Иллюстрируем проверку
assert (vl .size () >= v.size() && v2.size() >= v.sizeO);
//======== Иллюстрируем вторую версию transform
transform(v.begin(), v.end(), vl.begin(), v2.begin(),
plus
pr(v2,"Plus");
cout « "\n\n";
}
В рассмотренном фрагменте мы иллюстрируем использование алгоритма count_if, который проходит по заданному диапазону последовательности и возвращает количество объектов, удовлетворяющих предикату. Алгоритм f or_each позволяет выполнить определенное действие для заданного диапазона последовательности. В примере функция OutTeen вызывается для всех элементов контейнера. Приведем тело этой функции:
void OutTeen(Man& m)
{
// Если парамтр удовлетворяет предикату, то выводим его
if (Teen(m))
cout « "\nTeen: " « m;
}
Далее в коде демонстрируется, как использовать обратный итератор reverse_ iterator. Для него справедливы позиции rbegin — последний элемент последовательности и rend — барьер, ограничивающий последовательность спереди. Операция ++ сдвигает итератор на один элемент в сторону к началу последовательности.
Последний фрагмент функции main демонстрирует использование алгоритма transform, который воздействует на каждый элемент указанного диапазона и модифицирует его в соответствии либо с unary function (первая версия), либо с binary function (вторая версия). Суть модификации определяется последним параметром. В нашем случае мы используем negator (отрицатель) и бинарную операцию plus, настроенную на тип int. Сложить два контейнера можно и другими способами.
Если вы подключите файл заголовков
Assertion failed: s.topQ == joy,
file C:\My ProjectsXStack.cpp, line 29
abnormal program termination
Затем прекращает процесс вызовом функции abort. Если результатом выражения (аргумента функции assert) будет true, то выполнение продолжается.
Полезные константы
Полезные константы
STL имеет много полезных констант. Проверьте свои знания основ информатики. Знаете ли вы смысл констант, приведенных ниже? Для их использования вам потребуется подключить такие файлы заголовков:
#include
#include
#finclude
#finclude
Вот фрагмент, который выводит некоторые из констант и по-английски описывает их смысл. Русский язык отказывается работать на моем компьютере в окне консольного приложения. Думаю, что существуют программисты, которые зарабатывают свой хлеб, имея смутное представление о существовании этих констант:
//===== Сначала простые, которые знают все
cout « "\n Is a char signed? "
« numeric_limits
cout « "\n The minimum value for char is: "
« (int)numeric_limits
cout « "\n The maximum value for char is: "
« (int)numeric_limits
cout « "\n The minimum value for int is: "
« numeric_limits
cout « "\n The maximum value for int is: "
« numeric_limits
cout « "\n Is a integer an integer? "
« numeric_limits
cout « "\n Is a float an integer? "
« numeric_limits
cout « "\n Is a integer exact? "
« numeric_limits
cout « "\n Is a float exact? "
« numeric_limits
//===== Теперь более сложные
cout « "\n Number of bits in mantissa (double) : "
« DBL_MANT_DIG; cout « "\n Number of bits in mantissa (float): "
« FLT_MANT_DIG;
cout <<"\n The number of digits representble " "in base 10 for float is "
« numeric_limits
cout « "\n The radix for float is: "
« numeric_limits
cout « "\n The epsilon for float is: "
« numeric_limits
cout « "\n The round error for float is: "
« numeric_limits
cout « "\n The minimum exponent for float is: "
« numeric_limits
cout « "\n The minimum exponent in base 10: "
« numeric_limits
cout « "\n The maximum exponent is: "
« numeric_limits
cout « "\n The maximum exponent in base 10: "
« numeric_limits
cout « "\n Can float represent positive infinity? "
« numeric_limits
cout « "\n Can double represent positive infinity? "
« numeric_limits
cout « "\n Can int represent positive infinity? "
« numeric_limits
cout « "\n Can float represent a NaN? "
« numeric_limits
cout « "\n Can float represent a signaling NaN? "
« numeric_limits
//===== Теперь еще более сложные
cout « "\n Does float allow denormalized values? "
« numeric_limits
cout « "\n Does float detect denormalization loss? "
« numeric_limits
cout « "\n Representation of positive infinity for"
" float: "« numeric_limits
cout « "\n Representation of quiet NaN for float: "
« numeric_limits
cout « "\n Minimum denormalized number for float: "
« numeric_limits
cout « "\n Minimum positive denormalized value for"
" float " « numeric_limits
cout « "\n Does float adhere to IEC 559 standard? "
« numeric_limits
« numeric_limits
cout « "\n Is float modulo? "
« numeric_limits
cout « "\n is int modulo? "
« numeric_limits
cout « "\n Is trapping implemented for float? "
« numeric_limits
cout « "\n Is tinyness detected before rounding? "
« numeric_limits
cout « "\n What is the rounding style for float? "
« (int)numeric_limits
cout « "\n What is the rounding style for int? "
« (int)numeric_limits
//===== Теперь из другой оперы
cout « "\n Floating digits " « FLT_DIG;
cout « "\n Smallest such that 1.0+DBL_EPSILON !=1.0: "
« DBL_EPSILON;
cout « "\n LDBL_MIN_EXP: " « LDBL_MIN_EXP;
cout « "\n LDBL_EPSILON: " « LDBL_EPSILON;
cout « "\n Exponent radix: " « _DBL_RADIX;
Незнание констант типа DBL_EPSILON или DBL_MANT_DIG довольно сильно ограничивает квалификацию программиста, поэтому советую внимательно исследовать вывод, производимый данным фрагментом, и, возможно, обратиться к специальным изданиям по архитектуре компьютера или учебникам с целью ликвидировать пробелы в знаниях в этой области.
Последовательности типа deque
Последовательности типа deque
Контейнер типа deque (очередь с двумя концами) похож на vector в том смысле, что допускает выбор элемента по индексу и делает это быстро. Отличие состоит в том, что он умеет эффективно вставлять новые элементы как в конец, так и в начало последовательности. Deque не имеет некоторых методов, которые имеет vector, например capacity и reserve. Вместо этого он имеет методы, которых нет у вектора, например push_f ront, pop_back и pop_f ront. Далее мы будем исследовать возможности различных контейнеров, и каждый новый контейнер требует подключения своего файла заголовков. В данный момент не забудьте вставить директиву препроцессора tinclude
void main ()
{
deque
d.push_back(0.5) ;
d.push_back(l.);
d.push_front(-1.);
pr(d,"double Deque");
//======== Ссылки на два крайних элемента
deque
rf = d.front(),
rb = d.back();
//======== Присвоение с помощью ссылок
rf = 100.;
rb = 100.;
pr(d,"After using reference");
//======== Поиск с помощью связывателя
deque
bind2nd(less
//======== Вставка в позицию перед позицией,
//======== на которую указывает итератор
d.insert(p,-1.);
pr(d,"After find_if and insert");
//======== Второй контейнер
deque
//======== Вставка диапазона значений
d.insert (d.begin ()+1, dd.begin(), dd.end());
pr(d,"After inserting another deque");
cout«"\n\n";
}
Следующий фрагмент демонстрирует, как можно копировать контейнеры (сору) и обменивать данные между ними (swap). Шаблон функций find позволяет найти объект в любой последовательности. Он откажется работать, если в классе объектов не определена операция operator== (). Отметьте также, что после вставки или удаления элемента в контейнер типа deque все итераторы становятся непригодными к использованию (invalid), так как произошло перераспределение памяти. Однако удаление с помощью pop_back или pop_f ront портит только те итераторы, которые показывали на удаленный элемент, остальные можно использовать. При использовании фрагмент надо дополнить объявлениями объектов класса Man:
void main ()
{
deque
men.push_front (Man("Jimmy Young",16));
men.push_front (simon);
men.pushjoack (joy);
pr(men,"Man Deque");
//======== Поиск точного совпадения
deque
find(men.begin(),men.end() , joy);
men.insert(p,тагу);
pr(men,"After inserting тагу");
men.pop_back(); men.pop_front ();
pr(men,"After pop_back and pop_front");
p = find(men.begin(),men.end(),joy);
if (p == men.end())
cout « '\n' « joy « " not found!";
men.push_front(win); men.push_back(win);
pr(men,"After doubly push win");
//======== Второй контейнер
deque
//======== Копируем d в men
copy(d.begin(), d.end(), men.begin()); pr(men,"After resize and copy");
//======== Изменяем контейнер
d.assign(3,win);
//======== Обмениваем данные
d.swap(men);
pr(men,"After swap with another deque"); cout«"\n\n";
}
Последовательности типа list
Последовательности типа list
Контейнеры типа list представляют собой двусвязные списки, то есть упорядоченные последовательности, допускающие проходы как вперед, так и назад. Операции вставки и удаления одинаково эффективны в любое место списка. Однако операции поиска и выбора элементов линейны относительно размера контейнера. Выбор по индексу вовсе невозможен. Важным свойством списка является то, что операции вставки не портят итераторы, связанные с ним, а удаление делает недействительным только тот итератор, который указывал на удаленный элемент.
В шаблоне класса list определены методы merge, reverse, unique, remove и remove_if, которые оптимизированы для списков. Не путайте их с одноименными шаблонами функций, которые определены в алгоритмах. В примере, который приведен ниже, обратите внимание на операции слияния как списков, так и контейнеров различной природы. При исследовании списков не забудьте вставить директиву #include , а также приведенный выше набор объектов класса Man:
void main 0
{
list
men.push_front(zorah);
men.push_back(mela);
men.push_back(joy);
pr(men,"Man List");
//======== Поиск объекта
list
//======== Вставка перед элементом
p = men.insert (p,joe);
// одного объекта men.insert(p,2,joe);
// двух объектов pr(men,"After inserting 3 joes");
//======== Удаление всех вхождений joe
men.remove(j oe);
men.sort(less
pr(men,"After removing all joes and sort");
//======== Создаем второй список
list
//======== Сливаем его с первым
. men.merge (li, less
pr(men,"After merging with simons list");
//==== После слияния второй список полностью исчез
cout « "\n\tAfter merging simons li.size: "
« li.size() « endl;
men.remove(s imon);
//======== Создаем очередь
deque
//======== Копируем в нее список
copy(men.begin(), men.end(), d.begin());
pr(d,"Deque copied from list");
//======== Создаем вектор
vector
//==== Соединяем список и очередь и помещаем в вектор merge(men.begin(),men.end(),d.begin(),d.end(), v.begin() ) ;
pr(v,"Vector after merging list and deque");
pr(d,"Deque after merging with list");
cout«"\n\n";
}
После слияния (merge) двух списков (men и li) размер второго списка стал нулевым, так как он полностью влился в первый. При слиянии методом list: emerge элементы не копируются, а вливаются в список-мишень операции. При слиянии с помощью алгоритма merge контейнеры операнды остаются невредимыми, так как они копируются в контейнер-мишень. Если операнды операции слияния упорядочены, то при слиянии методом list::merge упорядоченность не нарушается, чего не наблюдается при слиянии шаблоном функции merge. Приведем для ясности результат работы рассматриваемой программы:
Man List # Sequence:
1. Zoran Todorovitch, Age: 27
2. Melissa Robinson, Age: 9
3. Joy Amore, Age: 18
After inserting 3 joes # Sequence:
1. Zoran Todorovitch, Age: 27
2. Joe Doe, Age: 30
3. Joe Doe, Age: 30
4. Joe Doe, Age: 30
5. Melissa Robinson, Age: 9 6. Joy Amore, Age: 18
After removing all joes and sort # Sequence:
1. Melissa Robinson, Age: 9
2. Joy Amore, Age: 18
3. Zoran Todorovitch, Age: 27
After merging with simons list # Sequence: 1. Melissa Robinson, Age: 9
2. Simon Paul, Age: 15
3. Simon Paul, Age: 15
4. Simon Paul, Age: 15
5. Joy Amore, Age: 18
6. Zoran Todorovitch, Age: 27
After merging Simons li.size: 0 Removing simons
Deque copied from list # Sequence:
1. Melissa Robinson, Age: 9
2. Joy Amore, Age: 18
3. Zoran Todorovitch, Age: 27
Vector after merging list and deque f Sequence:
1. Melissa Robinson, Age: 9
2. Joy Amore, Age: 18
3. Melissa Robinson, Age: 9
4. Joy Amore, Age: 18
5. Zoran Todorovitch, Age: 27
6. Zoran Todorovitch, Age: 27
Deque after merging with list # Sequence:
1. Melissa Robinson, Age: 9
2. Joy Amore, Age: 18
3. Zoran Todorovitch, Age: 27
С помощью алгоритма generate удобно создавать последовательности, имеющие строго определенную структуру. Например, если объявлен список целых из шести элементов, то его можно заполнить значениями, сгенерированными с помощью функции generate:
//========= Создаем список целых
list
//========= Генерируем степенную последовательность
generate (1st.begin (), Ist.end(), pows);
pr(1st,"List of generated powers");
Здесь pows — это внешняя функция, которая при каждом обращении возвращает возрастающую степень двойки. Эффект достигается за счет того, что static-переменная г инициализируется единицей во время компиляции, а затем помнит свое предыдущее значение:
uint pows()
{
static uint r = 1;
return r <= 1;
}
Если надо добиться обратного эффекта, то есть убрать закономерность в последовательности чисел, то можно воспользоваться шаблоном функции random_shuffle, которая переставляет элементы последовательности в одно из п! состояний. Например:
vector
for (int i = 0; i <= 6; i++ ) v.push_back(i+1);
random_shuffle(v.begin() , v.end()) ;
pr(v,"Vector of shuffled numbers");
Последовательности типа vector
Последовательности типа vector
Для их использования необходимо подключить файл заголовков
#include
Обратите внимание на отсутствие расширения .h в директиве подключения файла заголовков. Дело в том, что STL используется на множестве различных платформ с применением разных компиляторов. Файлы заголовков в разных условиях имеют разные расширения. Они могут иметь расширение Н, НРР или НХХ. Для того чтобы одинаково запутать всех разработчиков, было решено вовсе не использовать расширение для файлов заголовков библиотеки STL. Пространство имен std позволяет избежать конфликта имен. Если вы назовете какой-то свой класс тем же именем, что и класс из STL (например, string), то обращение вида std: : string однозначно определит принадлежность класса стандартной библиотеке. Директива using позволяет не указывать (многократно) операцию разрешения области видимости std: :, поэтому можно расслабиться и не писать std: : string, a писать просто — string.
Вектор является шаблоном класса, который настраивается с помощью двух параметров:
vector
Объект, который управляет динамическим выделением и освобождением памяти типа т, называется allocator. Для большинства типов контейнеров он обычно объявляется по умолчанию в конструкторе. Для «хитрых» данных, например требующих памяти в глобальном heap, видимо, можно изобрести индивидуальный распределитель памяти. Но в большинстве случаев работает вариант по умолчанию. Кроме того, с типом vector обычно связаны.4 типа сущностей:
Обычно эти сущности являются тем, чем и должны являться, но это не гарантируется. Они могут быть более сложными объектами.
Итак, vector является аналогом обычного массива в языке С, за исключением того, что он может автоматически изменять память по мере надобности. Доступ к данным обеспечивается с помощью операции выбора [ ]. Вставка новых элементов эффективна только в конец контейнера (push_back). Удаление — тоже. Данные операции в начале или середине влекут сдвиги всего массива данных, что резко снижает эффективность работы контейнера. Такие операции называются линейными, имея в виду тот факт, что время их выполнения линейно зависит от количества элементов в контейнере. Вставка или удаление в конце называются константными операциями, так как время их выполнения является константой для данной реализации и не зависит от количества элементов. Вот простая программа, иллюстрирующая использование вектора. Так как в приложениях консольного типа обычно возникают проблемы с русификацией, то для вывода текста мы используем английский язык:
#include
#include
#include
//======= Вводим тип для сокращения текста (места)
typedef unsigned int uint;
void main ()
{
//======== Вектор целых
vector
cout « "\nlnt Vector:\n";
for (uint i=0; i
{
v[i] = rand()%10 + 1;
cout « v[i] « "; ";
}
//======= Сортировка по умолчанию sort (v.begin (), v.end());
cout « "\n\nAfter default sort\n";
for (i=0; i
//======== Удаление элементов
v.erase(v.begin());
cout « "\n\nAfter first element erasure\n";
for (i=0; i
v. erase (v. end ()-2, v.endO);
cout « "\n\nAfter last 2 elements erasure\n";
for (i=0; i
cout « v[i] « "; ";
//======== Изменение размеров
int size = 2; v.resize(size);
cout « "\n\nAfter resize, the new size: " « v.size()
« endl; for (i=0; i
cout « v[i] « "; ";
v.resize(6,-1);
cout « "\n\nAfter resize, the new size: " « v.size()« endl;
for (i=0; i
cout « v[i] « "; ";
//======== Статистика .
cout « "\n\nVector's maximum size: " « v.max_size() « "XnVector's capacity: " « v.capacity() « endl
//======== Резервирование
v.reserve (100);
cout « "\nAfter reserving storage for 100 elements:\n"
« "Size: " « v.sizeO « endl :
« "Maximum size: " « v.max_size() « endl
« "Capacity: " « v.capacity() « endl;
v.resize(2000);
cout « "\nAfter resizing storage to 2000 elements:\n"
« "Size: " « v.size() « end1
« "Maximum size: " « v.max_size() « end1
« "Capacity: " « v.capacity() « endl; cout « "\n\n";
}
Для того чтобы лучше уяснить смысл и различие методов size, resize, max_size и capacity, мы несколько раз повторяем вызовы этих методов. Если вы читаете книгу вдалеке от компьютера, то вам, возможно, будет интересно узнать, что программа выведет в окно консольного приложения:
Int Vector:
2; 8; 5; 1;
After default sort
1; 2; 5; 8;
After first element erasure
2; 5; 8;
After last 2 elements erasure 2;
After resize, the new size: 2,
Vector capacity: 4 2; 0 ;
After resize, the new size:
6 2; 0; -1; -1; -1; -1;
Vector's maximum size: 1073741823
Vector's capacity: 6 After reserving storage for 100 elements:
Vector's size: 6
Vector's maximum size: 1073741823
Vector's capacity: 100
After resizing storage to 2000 elements:
Vector's size: 2000
Vector's maximum size: 1073741823
Vector's capacity: 2000
Демонстрация функционирования контейнеров требует часто выводить их содержимое, поэтому будет целесообразно написать шаблон функции, которая выводит содержимое контейнера любого типа. Первый параметр (т& v) функции рг () задает тип контейнера. Он же является параметром шаблона. Второй параметр (string s) задает строку текста (заголовок), который будет выведен в начале блока данных контейнера:
//===== Шаблон функции для вывода с помощью итератора
template
{
cout«"\n\n\t"«s«" # Sequence: \n";
//====== Итератор для любого контейнера
Т::iterator p;
int i;
for (p = v.begin(), i=0; p != v.end(); p++, i++)
cout « endl « i + 1 «". "« *p; cout « '\n';
}
Для пробега по всем элементам любого контейнера используется обобщенный, или абстрактный, указатель, который объявлен как т:: iterator. С помощью итератора, так же как и с помощью обычного указателя, можно получить доступ к элементу, используя операции *, ->. К нему также применима операция ++ — переход к следующему элементу последовательности, но в отличие от указателя с итератором не связана адресная арифметика. Вы не можете сказать, что значение итератора изменится на 4 байта при переходе к следующему элементу контейнера целых чисел, хотя для некоторых типов контейнеров это так и будет. Заметьте, что операция ++ в применении к итератору позволяет перейти к следующему элементу как вектора — элементы расположены в памяти подряд, так и списка — элементы расположены в памяти не подряд. Поэтому итератор — это более сложный механизм доступа к данным, чем простой указатель. Полезно представить итератор в виде рабочего, приставленного к контейнеру и призванного перебирать его элементы.
Возможное присвоение p = v. end (); не означает, что итератор устанавливается на последний элемент последовательности. Вы помните, какую роль играет ноль для обычного указателя при работе с динамическим списком? Примерно такую же роль для итератора выполняет значение v. end () — конец последовательности. Его можно представить в виде итератора, указывающего на воображаемый элемент, следующий за последним элементом контейнера (past-the-end value). Однако инициализатор p = v.begin (); устанавливает итератор в точности на первый элемент последовательности.
Следующий фрагмент демонстрирует работу с вектором строк типа string. Теперь в контейнере будут храниться объекты класса string, который также является составной частью STL. Этот класс содержит ряд замечательных методов, которые позволяют легко и удобно работать со строками символов произвольной длины. В частности, вы можете складывать строки — осуществлять их конкатенацию, искать подстроки, удалять и заменять подстроки:
#include
#include
#include
#include
using namespace std;
void main ()
{
//========= Вектор строк текста
vector
v.push_back("pine apple");
v.push_back("grape") ;
v.push_back("kiwi fruit");
v.push_back("peach") ;
v.push_back("pear") ;
v.push_back("apple") ;
v.push_back("banana") ;
//========= Вызываем наш шаблон вывода
pr(v, "String vector");
sort (v.begin () , v.end());
pr(v, "After sort"); '
//========= Изменяем порядок сортировки, на обратный
//========= тому, который принят по умолчанию
sort(v.begin(), v.end(), greater
pr(v, "After predicate sort");
cout « "\nDistance from the 1st element to the end: ";
vector
vector
d = distance(p, v.end());
//========= Отметьте, что end() возвращает адрес
//========= за концом последовательности
cout « d « endl;
cout « "\n\nAdvance to the half of that distanceXn";
advance (p, d/2);
cout « "Now current element is: " « *p « endl;
d = distance(v.begin (), p);
cout « "\nThe distance from the beginning: " « d « endl;
d = distance(p, v.begin ());
cout « "\nThe distance to the beginning: "
« d « endl;
}
Здесь мы демонстрируем, как можно с помощью бинарного предиката greater <Туре> изменить порядок сортировки элементов последовательности. Предикатом называется функция, область значений которой есть множество { false, true } или { 0, 1 }. В нашем случае этот предикат пользуется результатом операции operator > (), определенной в классе string. Кроме того, мы показываем, как можно пользоваться шаблоном функций distance, который позволяет определить количество приращений типа dif ference_type между двумя позициями, адресуемыми итераторами. Другой шаблон функций — advance позволяет продвинуться вдоль контейнера на число позиций, задаваемое параметром, который может быть и отрицательным.
Предикатом, как определено в курсе математической логики, называется любая функция многих переменных, областью значений которой является множество {false, true} или {0, 1}. Покажем, как можно отсортировать по имени контейнер объектов класса Man, который мы определили в этом уроке выше. Алгоритм sort по умолчанию использует для сортировки бинарное отношение, задаваемое операцией operator< (). Так как в классе Man эта операция была определена в виде метода класса, то алгоритм справится с поставленной задачей. Однако если мы захотим изменить порядок и отсортировать последовательность объектов по возрасту, то нам придется воспользоваться другим отношением. Решить эту задачу можно двумя способами:
Первый способ реализуется путем создания глобальной функции, на вход которой поступают два сравниваемых объекта, а на выходе должен быть результат их сравнения, например типа bool. Второй способ реализуется созданием функционального объекта (function object или functor), являющегося структурой, в которой определена операция operator (). Этот термин, однако, используется для обозначения не только описанного объекта, но и для любого другого, который можно вызвать так, как вызывают функцию. Собственно, кроме описанного случая, роль функционального объекта может выполнять обычная функция и указатель на функцию.
Покажем, как создать предикат. В описание класса Man следует добавить объявление внешней функции в качестве friend-объекта, так как в ее теле будут анализироваться private-данные класса Man. Добавьте в класс Man такое описание:
//======== Предикат, задающий отношение порядка
friend bool LessAge (Mans a, Man& b);
Затем вставьте коды этой функции после объявления класса, но до тела функции main:
bool LessAge (Man& a, Man& b)
{
//======== Сравниваем по возрасту
return a.m_Age < b.m_Age;
}
Теперь можно создать контейнер объектов класса Man и убедиться в возможности его сортировки двумя способами. В момент создания контейнер может быть инициализирован элементами обычного массива. Ниже мы показываем, как это сделать:
void main ()
{
//======== Массив объектов класса Man
Man ar[] =
{
Man("Mary Poppins",36),
Man("Joe Doe",30),
Man("Joy Amore",18),
Man("Zoran Todorovitch",27)
};
uint size = sizeof(ar)/sizeof(Man);
//======== Создаем контейнер на основе массива
vector
//======== Реверсируем обычный массив
reverse(ar, ar+size);
cout « "\n\tAfter reversing the array\n\n";
for (uint i=0; i
cout « i+1 « ". " « ar[i] « '\n';
//======== Сортиуем по умолчанию
sort (men.begin (), men.endO);
pr(men,"After default sort");
//======== Используем предикат
sort (men .begin () , men.endO, LessAge);
pr(men,"After predicate LessAge sort");
cout « "\n\n";
}
Алгоритм переворота последовательности (reverse) может работать как с контейнером, так и с обычным массивом. Для успешной работы ему надо задать диапазон адресов (range). Обратите внимание на то, что в качестве конца последовательности этому алгоритму, как и многим другим в STL, надо подать во втором параметре адрес ar+size, выходящий за пределы массива. Как объяснить тот факт, что шаблон функции reverse, требуя в качестве параметров переменные типа iterator, тем не менее работает, когда ему подают обычный указатель типа Man*? В документации вы можете найти такое объяснение. Указатель — это итератор, но итератор — это не указатель. Итератор — это обобщение (generalization) указателя.
Результат работы программы выглядит так:
Man Vector # Sequence:
1. Mary Poppins, Age: 36
2. Joe Doe, Age: 30
3. Joy Amore, Age: 18
4. Zoran Todorovitch, Age: 27
After reversing the array
1. Zoran Todorovitch, Age: 27
2. Joy Amore, Age: 18
3. Joe Doe, Age: 30
4. Mary Poppins, Age: 36
After default sort # Sequence:
1. Joe Doe, Age: 30
2. Joy Amore, Age: 18
3. Mary Poppins, Age: 36 /
4. Zoran Todorovitch, Age: 27
After predicate LessAge sort # Sequence:
1. Joy Amore, Age: 18
2. Zoran Todorovitch, Age: 27
3. Joe Doe, Age: 30
4. Mary Poppins, Age: 36
Здесь мы убрали сообщения, которые выводит деструктор класса Man. Они носят чисто учебный характер. В частности, они позволяют обозначить те моменты из жизни объектов класса Man, когда они уничтожаются, то есть когда система вызывает для них деструктор.
Сейчас мы намерены создать функциональный объект и использовать его для выбора режима сортировки по имени или по возрасту. Текущий выбор удобно хранить в static-переменной класса Man. Такие переменные, как вы знаете, являются общими для всех объектов класса. Изменяя их значение, можно управлять общими установками, касающимися всех объектов класса. Мы будем управлять текущим режимом сортировки. Для удобства чтения кода введем глобальное определение типа SORTBY - перечисление режимов сортировки:
enum SORTBY { NAME, AGE }; // Режимы сортировки
Декларацию static-переменной следует вставить в public-секцию класса Man:
static SORTBY m_Sort; // Текущий режим сортировки
Определение static-переменной, согласно законам ООП, должно быть глобальным:
//======= Определение и инициализация
SORTBY Man::m_Sort = NAME;
Сам функциональный объект должен быть объявлен как внешняя глобальная friend-конструкция. Вставьте следующее объявление внутрь класса Man:
//======= Функциональный объект имеет доступ к данным
friend struct ManLess;
Затем объявите сам функциональный объект, который, надо признать, имеет несколько непривычный вид:
//======== Функциональный объект
struct ManLess
{
bool operator()(Man& a, Man& b)
{
return a.m_Sort==NAME ? (a.m_Name < b.m_Name)
: (a.m_Age < b.m_Age);
}
};
Как вы видите, он имеет вид структуры (возможен и класс), в которой определена единственная операция operator (). В нем мы анализируем текущее значение флага сортировки и в зависимости от него возвращаем результат сравнения двух объектов, поступивших в качестве параметров, по тому или иному полю. Использование функционального объекта иллюстрируется следующим кодом, который вы можете вставить в конец существующей функции main:
//========= Используем функциональный объект
Man::m_Sort = NAME;
//========= Сортируем по имени
sort (men .begin (), men.end(), ManLess ());
pr(men,"After function object name sort");
Man::m_Sort = AGE;
//========= Сортируем по возрасту
sort (men. begin (), men.end(), ManLess ());
pr(men,"After function object age sort");
Аналогично предикату greater
//========= Используем стандартный предикат
sort(men.begin(), men.end(),less
pr(men,"After less
то получите сообщение об ошибке, так как он будет искать реализацию operator< () в виде внешней функции с двумя сравниваемыми параметрами. Напомню, что мы уже реализовали эту операцию, но в виде метода класса с одним параметром. Для решения проблемы вы можете, не убирая старой версии, вставить новую. Декларируйте в классе Man внешнюю friend-функцию:
//========= Нужна для предиката less
friend bool operator< (const Man& a, const Man& b);
Затем дайте внешнее тело этой функции. Отношение порядка здесь намеренно изменено по сравнению с предыдущей реализацией operators (). Как оказалось, обе версии будут работать в различных ситуациях. Первая — при сортировке по умолчанию, а вторая — при сортировке предикатом less
bool operator<(const Man& a, const Man& b)
{
//======== Сравниваем по возрасту
return a.m_Age < b.m_Age;
}
Проверьте результат, запустив приложение. Проследите, чтобы в main был при этом код с вызовом алгоритма сортировки с тремя Параметрами:
sort(men.begin (), men.end(),less
Здесь же уместно добавить, что в STL есть шаблоны, которые называются negators (отрицатели). Шаблон not2, например, позволяет инвертировать результат бинарной операции. Вставьте в конец функции main следующий фрагмент:
//========= Используем отрицатель бинарной операции
sort(men.begin (), men.endf), not2 (less
pr(men,"After not2(less
и убедитесь в том, что последовательность отсортирована теперь по убыванию возраста.
Примеры использования string
Примеры использования string
Тип string является специализацией шаблона basic_string для элементов типа char и определен как:
typedef basic_string
Шаблон basic_string предоставляет типы и методы, схожие с теми, что предоставляют стандартные контейнеры, но он имеет много специфических методов, которые позволяют достаточно гибко манипулировать как строками, так и их частями (подстроками). Минимизация операций копирования строк, которой гордится MFC-класс cstring, на самом деле приводит к труднообнаруживаемым и невоспроизводимым (irreproducible) ошибкам, которые очень сильно портят жизнь программистам. Я с интересом узнал, что члены комиссии по утверждению стандарта C++ анализируют ошибки, возникающие из-за совместного использования двумя переменными строкового типа одной и той же области памяти, и пытаются выработать спецификации относительно времени жизни ссылок на символы строки. Если вы запутались в этой фразе, то следующий фрагмент программы, который комиссия использует в качестве теста, должен прояснить ситуацию. При выполнении он выведет строку «Wrong» или «Right», что означает, что ваша реализация string ненадежна или, скорее всего, надежна. Если она выведет строку «Right», то это еще не означает, что ваша реализация надежна. Ошибки могут всплыть в многопоточных приложениях, когда разные потоки работают с одной строкой символов:
//====== Две тестовые текстовые строки
string source("Test"), target;
//====== Ссылка на второй символ в строке
char& с = source[1];
//=====- Если данные не копируются при присвоении
target = source;
//====== то это присвоение изменит обе строки
с = ' z ' ;
//====== Этот тест позволяет выяснить ситуацию
cout « (target[l] == 'z1 ? "\nWrong" : "\nRight");
Здесь мы использовали ссылку, но аналогичное поведение обнаруживает и итератор. Вы можете объявить и использовать его так:
string::iterator it = source.begin()+1; *it = z1 ;
В рассматриваемой версии Studio.Net я с удовлетворением отметил, что тест выводит строку «Right». Следующий фрагмент демонстрирует технику обрезания «пустого» текста в начале и конце строки. Она не очень эффективна, но вполне пригодна для строк небольшого размера:
//====== Множество пустых символов
char White " \n\t\r";
//====== Ищем реальное начало строки
//====== и усекаем лишние символы слева
s = s.substr(s.find_first_not_of(White));
//====== Переворачиваем строку и повторяем процедуру
reverse (s .begin () , s.endO);
s = s.substr(s.find_first_not_of(White));
//====== Вновь ставим строку на ноги
reverse (s .begin (), s.end());
Интересный пример, иллюстрирующий работу со строками, я увидел в MSDN. Нечто вроде секретного детского языка под названием Pig Latin (свинячья латынь). Алгоритм засекречивания слов состоит в том, что от каждого слова отрывают первую букву, переставляют ее в конец слова, а затем добавляют туда окончание «ау». Игра, очевидно, имеет свою историю. Приведем коды функции, которая реализует этот алгоритм и возвращает засекреченную строку:
//====== Преобразование строки по принципу Pig Latin
string PigLatin (const strings s)
{
string res;
//======= Перечень разделителей слов
string sep(" .,;:?");
//======= Длина всей строки
uint size = s.lengthO;
for (uint start=0, end=0, cur=0; cur < size; cur=end+l)
{
//==== Ищем позицию начала слова, начиная с cur
start = s.find_first_not_of(sep, cur) ;
//==== Копируем разделители между словами
res += s.substr(cur, start - cur) ;
//==== Ищем позицию конца слова, начиная со start
end = s.find_first_of(sep, start) ;
//==== Корректируем позицию конца слова
end = (end >= size) ? size : end - 1 ;
//==== Преобразуем по алгоритму
res += s. substr (start-t-1, end-start) + s [start] +"ay"; )
return res;
}
Проверьте работу алгоритма с помощью следующего теста, который надо вставить внутрь функции main:
string s("she,sells;
sea shells by the sea shore");
cout « "Source string: " « s « endl;
cout « "\nPig Latin(s): " « PigLatin(s);
В результате вы увидите такой текст:
Source string: she,sells;
sea shells by the sea shore
Pig Latin(s): hesay,ellssay;
easay hellssay ybay hetay easay horesay
Работа с потоками
Работа с потоками
Шаблон класса if stream позволяет работать с файловыми потоками и производить ввод объектов произвольного типа. Удобно вводить объекты прямо в контейнер. Специальный итератор (istream_iterator) помогает в этом случае воспользоваться алгоритмами (например, сору). При достижении конца потока (end of stream) итератор принимает специальное значение, которое служит барьером выхода за пределы потока (past-the-end iterator). В примере, приведенном ниже, используется еще один тип итератора (back_insert_iterator). Он является адаптером, позволяющим вставлять элементы в конец последовательности. Если использовать прямой inserter, то при чтении из файла последовательность будет реверсирована (перевернута). Позиционирование в потоке осуществляется с помощью метода seekg, техника использования которого также демонстрируется в примере:
void main ()
{
//========== Вектор строк
vector
v.push_back("Something in the way ");
v.push_back("it works distracts me ");
v.push_back("like no other matter");
pr(v,"Before writing to file");
//========== Запрашиваем имя файла
cout « "\nEnter File Name: ";
string fn, text; cin » fn;
//========== Приписываем расширение
int pos = fn.rfind(".");
if (pos > 0)
fn.erase(pos);
fn += ".txt";
//========== Создаем и открываем поток
ofstream os(fn.c_str());
//========== Определяем входной и выходной потоки
typedef istream_iterator
char_traits
typedef ostream_iterator
char_traits
//========== Копируем контейнер в выходной поток
copy (v.begin(), v.end(), StrOut(os,"\n"));
os.close();
//========== Открываем файл для чтения
if stream is(fn.c_str());
//========= Пропуск 17 символов
is.seekg(17) ;
is » text;
cout « "\n\nStream Positioning:\n\n" « "17 bytes:\t\t" « text « endl;
//========== Устанавливаем в начало потока
is.seekg(0, ios_base::beg);
is » text;
cout « "0 bytes:\t\t" « text « endl;
//========== Сдвигаем на 8 символов от конца
is.seekg(-8, ios_base::end);
is » text;
cout « "-8 bytes from end:\t" « text « "\n\n";
//========== Устанавливаем в начало потока
is.seekg(0, ios_base::beg);
v.clear () ;
//========== Копируем в контейнер
copy(Strln(is),Strln(),back_inserter(v));
pr(v,"After reading from file");
cout«"\n\n"; }
Программа производит следующий выход:
Before writing to file # Sequence:
1. Something in the way
2. it works distracts me
3. like no other matter
Enter File Name: test
Stream Positioning:
17 bytes: way
0 bytes: Something
-8 bytes from end: matter
After reading from file # Sequence:
1. Something
2. in
3. the
4. way
5. it
6. works
7. distracts
8. me
9. like
10. no
11. other
12. matter
Сечения массива
Сечения массива
Проблемы оптимизации работы с матрицами давно волнуют создателей компиляторов. В то далекое время, когда решения задач электродинамики и вообще краевых задач матфизики еще интересовали влиятельных людей нашей страны (скорее, научные авторитеты убеждали их, что такие задачи следует решать), мы, используя язык PL/I или FORTRAN, конечно же, хранили и обрабатывали матрицы в одномерных массивах. Дело в том, что выбор одного элемента из более естественного для матриц двухмерного массива обходился дорого. Выработалась особая техника работы с одномерными массивами, хранящими матрицы (обычно разреженные). В языке C++ операция выбора элемента из двухмерного динамического массива не намного дороже, чем из одномерного (да и скорости изменились), поэтому острота проблемы спала. Тем не менее проблема экономии времени при решения сложных краевых задач не ушла в прошлое.
STL имеет пару вспомогательных классов: slice и gslice, которые созданы для того, чтобы было удобно работать со срезами (сечениями) одномерных массивов. Если вы храните двухмерную матрицу в последовательности типа valarray, то элементы одной строки матрицы или одного ее столбца можно представить в виде сечения, то есть определенной части всей последовательности. Конструктор класса slice определяет закономерность, в соответствии с которой будут выбираться элементы последовательности, чтобы образовать срез. Например, объект slice s(0, n , 2); представляет собой сечение из п элементов последовательности. Элементы выбираются начиная с нулевого, через один, то есть с шагом 2. Если вы храните матрицу пхп в последовательности типа valarray и при этом она упорядочена по строкам (сначала первая строка, затем вторая, и т. д.), то третью строку матрицы можно выбрать с помощью сечения:
slice s (2*n, n , 1);
Действительно, параметры указывают, что надо пропустить 2*n элементов, затем выбрать n элементов с шагом по одному. Если матрица хранится a la FORTRAN, то есть по столбцам, то для выбора той же строки надо определить сечение:
slice s (2, n , n);
Пропускаются два элемента, затем выбирается n элементов с шагом п. Вы, конечно, поняли, как создать сечение, но не поняли, какое отношение оно имеет к последовательности valarray, так как она не фигурирует в приведенных выражениях. Да, синтаксис, связывающий срез с valarray, несколько необычен, хотя вполне логичен:
int n = 5, // Размерность матрицы n (размером пхп) пп = п*п;
// Размерность valarray
//=== Создаем матрицу (одномерную последовательность)
valarray
//=== Генерируем ее элементы по закону f (Пока его нет)
generate (&a[0], &a[nn], f) ;
//====== Создаем сечение
slice s (0, n , 1);
//====== Выделяем сечение (первую строку,
//====== если матрица хранится по строкам)
valarray
Вы видите, что объект s класса slice помещается в то место, куда мы обычно помещаем целочисленный индекс массива или последовательности. Такая интерпретация операции [ ] непривычна. Вы, вероятно, догадались, что роль объекта s в приведенном фрагменте является чисто эпизодической. Можно обойтись и без него, заменив его временным безымянным объектом, который создаст компилятор. При этом конструкция выражения будет более эффективной, но и более головоломной. Последние две строки фрагмента можно заменить одной строкой:
valarray
Подведем итоги. В этом уроке мы оценили возможности библиотеки STL и сделали вывод, что она, очевидно, имеет гораздо больше достоинств, чем недостатков. Необходимо регулярно тренировать технику ее использования. В этой задаче может помочь сеть Интернет, в которой появляется все больше сайтов, уделяющих внимание STL. Кроме того, мы:
Шаблон функции быстрой сортировки
Шаблон функции быстрой сортировки
Приведем пример реализации вышеупомянутого рекурсивного алгоритма сортировки массива переменных Quicksort. Его идея состоит в том, что меняются местами элементы массива, стоящие слева и справа от выбранного «центрального» (mid) элемента массива, если они нарушают порядок последовательности. Интервал, в котором выбирается центральный элемент, постепенно сжимается, «расправляясь» сначала с левой половиной массива, затем с правой. Функция Quicksort, приведенная ниже, реализует рекурсивный алгоритм быстрой сортировки. Далее следует код, который позволяет протестировать работу функции. Сортируется массив вещественных чисел, элементы которого заданы случайным образом:
void Quicksort (double *ar, int 1, int r)
{
//========== Рабочие переменные
double mid, temp;
//========== Левая и правая границы интервала
int i = 1, j = r;
//========== Центральный элемент
mid = ar[ (1 + г) /2];
//========== Цикл, сжимающий интервал
do
//== Поиск индексов элементов, нарушающих порядок
while (ar[i] < mid)
i++; // слева
while (mid < ar[j])
j--; // и справа
//== Если последовательность нарушена,
if (i <= j)
{
//===== то производим обмен
temp = ar[i];
ar[i++] = ar[j];
ar[j-—] = temp;
}
}
//========= Цикл do-while повторяется, пока
//========= есть нарушения последовательности
while (i <= j);
//========= Если левая часть не упорядочена,
if (I < j)
Quicksort (ar, 1, j); // то занимаемся ею
// Если правая часть не упорядочена,
if (i < r)
Quicksort (ar, i, r); // то занимаемся ею }
//========== Тестируем алгоритм
void main()
{
//========= Размер массива сортируемых чисел
const int N = 21;
double ar[N]; // Сам массив
puts("\n\nArray before Sorting\n");
for (int i=0; i
ar[i] = rand()%20;
if (i%3==0)
printf ("\n");
printf ("ar[%d]=%2.0f\t",i,ar[ij);
}
Quicksort(ar,0,N-1); // Сортировка
puts("\n\nAfter SortingNn");
for (i=0; i
{
if (i%3==0)
printf ("\n");
printf ("ar[%d]=%2.0f\t",i,ar[i]);
}
puts ("\n");
}
Для того чтобы сортировать массивы любого типа, целесообразно на основе данной функции создать шаблон. Оказывается, что для этого нужно приложить совсем немного усилий. В уже существующий код внесите следующие изменения:
template
void Quicksort (Т *ar, int 1, int r)
{
//======= Рабочие переменные
Т mid, temp;
//======= Далее следует тот же код, который приведен
//======= в оригинальной версии функции Quicksort
}
Проверьте функционирование, вставив в функцию main вызовы функции с другими типами параметров. Например:
void main()
{
//======= Размер массива сортируемых чисел
const int N = 21;
// double ar[N];
int ar[N];
puts("\n\nArray before SortingXn");
for (int i=0; i
{
ar[i] = rand()%20; if (i%3==0)
printf ("\n"); // printf ("ar[%d]=%2.0f\t",i,ar[i]);
printf ("%d\t",ar[i]);
}
Quicksort(ar,0,N-1);
puts("\n\nAfter SortingXn");
for (i=0; i
{
if (i%3==0)
printf ("\n"); // printf ("ar[%d]=%2.0f\t",i,ar[i]);
printf ("%d\t",ar[i]);
}
puts("\n");
}
В данный момент функция main настроена на сортировку массива целых. Внесите приведенные изменения и проверьте работу шаблона для массива целых. Уберите комментарии там, где они есть, но вставьте комментарии в строки программы, следующие ниже. После этой процедуры функция main будет настроена на проверку шаблона функции сортировки для массива вещественных. Проверьте и этот случай. Шаблон должен справиться с обоими.
Примечание 1
Примечание 1
Перед запуском консольных приложений настройте консольное окно так, чтобы его размеры вмещали весь выходной текст. Для этого вызовите контекстное меню на заголовке консольного окна и дайте команду Properties. Откройте страницу на вкладке Layout и подстройте размеры окна в полях Width и Height группы Window Size.
Шаблон классов valarray
Шаблон классов valarray
Этот шаблон разработан для оптимизации вычислений, производимых над массивами чисел фиксиррванного размера. Valarray похож на контейнер, но он им не является. Вы не можете динамически и эффективно наращивать его размер. Он, как и контейнер, может изменять свои размеры, используя метод resize, но при этом имеющиеся данные разрушаются. Главным преимуществом использования valarray является эффективность проведения операций сразу над всеми элементами последовательности. Предположим, вы хотите построить график функции у = sin(x) и имеете процедуру, которая сделает это с учетом масштабирования, оцифровки осей и всяких других удобств. Вашей задачей является лишь сформировать данные для графика и подать их на вход этой процедуры. Использование valarray даст преимущество в легкости манипулирования данными и эффективности выполнения. Для простоты выберем шаг изменения координаты х, равный л/3,
Примечание 1
Примечание 1
C целью экономии места я обычно не привожу директивы препроцессора, которые, конечно же, должны предшествовать каждому из рассматриваемых фрагментов. Большинство читателей, я уверен, успешно решают эту проблему сами, так как сообщения об ошибках обычно довольно ясно указывают на недостающее описание. Но при работе с библиотекой STL окно сообщений ведет себя не совсем так, как при работе с MFC. Незначительный пропуск или неточность со стороны программиста порой приводят к лавине предупреждений и ошибок, анализ которых превращается в испытание для нервной системы. Здесь у компании Microsoft еще довольно много работы. Учитывая сказанное, следующий фрагмент приведен со списком директив, необходимых для его работы.
#include
#include
#include
#include
using namespace std;
void main() { //======== Вспомогательные переменные
double PI = atan(l.)*4.,
dx = PI/3., // Шаг изменения
xf = 2*PI - dx/2.;
// Барьер
int i = 0,
size = int(ceil(xf/dx)); // Количество точек
//======== Создаем два объекта типа valarray
valarray
//======== Абсциссы точек вычисляются в цикле
for (double х=0.;
х < xf; х += dx) vx[i++] = х;
//======== Ординаты вычисляются без помощи цикла
vy = sin(vx);
cout«"Valarrays of x and sin(x)\n";
for (i=0; i < size; i++)
cout«"\nx = " « vx[i] «" у = "« vy[i];
}
Теперь усложним задачу. Представим, что надо численно продифференцировать функцию, заданную в дискретном множестве точек. Вы знаете, что конечные разности позволяют аппроксимировать производные, то есть производить численное дифференцирование. В STL есть алгоритм adjacent_dif ference, который вычисляет первые конечные разности в указанном диапазоне последовательности. Здесь важно вспомнить, что valarray не является контейнером и поэтому не поддерживает итераторов. Но алгоритмы STL принимают в качестве аргументов как итераторы, так обычные указатели. Мы воспользуемся этим фактом, а также тем, что элементы valarray расположены в памяти подряд.
Результат дифференцирования надо поместить в другую последовательность типа valarray, которую после этого можно эффективно нормировать, поделив сразу все ее элементы на шаг дискретизации вдоль оси х. Добавьте директиву # include
//======= Конструктор создает valarray нужного размера
valarray
//======= Алгоритм вычисляет конечные разности
adjacent_difference(&vy[0], &vy[size], &vd[0]);
//======= Все элементы valarray делятся на dx
vd /= dx;
//======= Мы проверяем результат
cout«"\n\nValarray of differences\n";
for (i=l; i < size; i++)
cout«"\nx = " « vx[i] «" у = "« vd[i];
Отметьте, что в первой точке (с нулевым индексом) будет ошибка, поэтому мы ее не выводим. Остальные элементы результирующей последовательности чисел (valarray vd) должны вести себя как у = cos(x). В качестве третьего параметра функции adjacent_dif ference нельзя задать просто vd, так как в отличие от обычного массива имя vd не является адресом его первого элемента. Шаблон классов valarray имеет некоторое, весьма ограниченное количество методов, которые позволяют производить манипуляции с данными, среди которых стоит отметить: min, max, sum, shift, cshift, apply. Приведем фрагмент, иллюстрирующий их использование:
//======= Функциональный объект, применяемый к каждому
//======= элементу valarray
double Sharp (double x)
{
return x != 0. ? l/(x*x) : DBL_MAX;
}
//======= Функция для вывода valarray
void out(char* head, valarray
{
cout « '\n' « head << '\n';
for (unsigned i=0; i < v.size(); i++)
cout«"\nv[" « i « "] = " « v[i];
cout «'\n';
}
void main()
{
int size = 11;
valarray
//======== Заполняем диапазон от -1 до 1
for (int i=0; i < size; i++)
{
vx[i] = i/5. - 1.;
}
out("Initial valarray", vx);
//======== Вычисляем сумму всех элементов
cout « "\nsum = " « vx.sum() « endl;
//======== Применяем свое преобразование
vy = vx.apply (Sharp);
//======== Получили "острую" функцию
out("After apply", vy);
//======== Вычисляем min и max
cout « "\n\nmin = " « vy.min() « " max = " « vy.max();
}
При положительных значениях аргумента метод shift используется для сдвига всей последовательности влево или при отрицательных значениях — вправо. Метод cshif t представляет собой циклическую модификацию метода shift. Заметьте, что все рассмотренные методы возвращают новую последовательность типа valarray и не имеют модификаций, работающих в режиме in-place, что, на мой взгляд, является ощутимым недостатком этого типа данных. Вы можете проверить работу сдвигов, добавив такие строки:
//======== Циклический сдвиг на 2 позиции влево
valarray
out("After cyclic 2 digits left shift", r) ;
//======== Сдвиг на 2 позиции вправо
r =r.shift(-2);
out("After 2 digits right shift", r);
Шаблоны классов
Шаблоны классов
Шаблон классов (class template) в руководствах программиста иногда называется generic class или class generator. Шаблон действительно помогает компилятору сгенерировать определение конкретного класса по образу и подобию заданной схемы. Разработчики компилятора C++ различают два термина: class template и template class. Первый означает абстрактный шаблон классов, а второй — одно из его конкретных воплощений. Пользователь может сам создать template class для какого-то типа данных. В этом случае созданный класс отменяет (overrides) автоматическую генерацию класса по шаблону для этого типа данных. Рассмотрим стандартный пример, иллюстрирующий использование шаблона для автоматического создания классов, которые реализуют функционирование абстрактного типа данных «Вектор линейного пространства». Элементами вектора могут быть объекты различной природы. В примере создаются векторы целых, вещественных, объектов некоторого класса circle (вектор окружностей) и указателей на функции. Для вектора из элементов любого типа тела методов шаблона одинаковы, поэтому и есть смысл объединить их в шаблоне:
#include
#include
#include
//====== Шаблон классов "Вектор линейного пространства"
template
{
//====== Данные класса
private:
Т *data; // Указатель начала массива компонентов
int size; // Размер массива
//====== Методы класса
public:
Vector(int);
~Vector()
{
delete[] data;
}
int Size()
{
return size;
}
T& operator [](int i)
{
return data[i];
}
};
//====== Внешняя реализация тела конструктора
template
{
data = new Т[n];
size = n; };
//====== Вспомогательный класс"Круг"
class Circle
{
private:
//====== Данные класса
int х, у; // Координаты центра
int r; // Радиус
public:
//====== Два конструктора
Circle ()
{
х = у = r = 0; }
Circle (int a, int b, int с) {
x = a;
У = b;
r = с;
}
//====== Метод для вычисления площади круга
double area ()
{
return 3.14159*r*r;
}
};
//====== Глобальное определение нового типа
//====== указателя на функцию
typedef double (*Tfunc) (double);
//====== Тестирование ч
void main ()
{
//===== Генерируется вектор целых
Vector
for ( int i=0; i < x.SizeO; ++i)
{
x[i] = i; // Инициализация
cout « x[i] « ' ' ; // Вывод
}
cout « ' \n ' ;
//===== Генерируется вектор вещественных Vector
for (i=0; i < y.SizeO; ++i)
{
y[i] = float (i); // Инициализация cout « y[i] « ' ' ; // Вывод
}
cout « ' \n' ;
//==== Генерируется вектор объектов класса Circle
Vector
for (i=0; i< z.SizeO; ++i) // Инициализация
z[i] = Circle(i+100,i + 100,i+20) ;
cout « z[i].area() « " "; // Вывод
}
cout « ' \n' ;
//==== Генерируется вектор указателей на функции
Vector
cout«"\nVector of function pointers: " ;
f[0] = sqrt; // Инициализация
f[l] = sin;
f[2] = tan;
for (i=0; i< f.Size(); ++i)
cout « f[i](3.) « ' '; // Вывод cout « "\n\n";
}
Обратите внимание на синтаксис внешней реализации тела конструктора шаблона классов. Vector
В рассмотренном примере операция [ ] определена в шаблоне как общая для всех типов Т, однако метоД area () определен только для объектов класса Circle и он применяется к объекту z [i] класса circle, вектор из четырех элементов которого автоматически создается компилятором при объявлении Vector
Если для какого-то типа переменных автоматически сгенерированный по шаблону класс не подходит, то его следует описать явно. Созданный таким образом класс (template class) отменяет автоматическое создание класса по шаблону только для этого типа. Например, предположим, что создан новый класс Man:
class Man
{
private:
string m_Name;
int m_Age;
public:
//======= Конструкторы
Man{}
{
m_Name = "Dummy";
m_Age = 0; }
Man (char* n, int a)
{
m_Name = string(n); m_Age = a;
}
Man (strings n, int a)
{
m_Name = n;
m_Age = a;
}
Man& operator=(const Man& m)
{
m_Name = m.m_Name;
m_Age = m.m_Age;
return *this;
}
Man(const Man& m)
{
*this = m;
}
//======== Деструктор
~Man()
{
cout « "\n+ + " « m_Name « " is leaving";
m_Name.erase (); }
bool operator==(const Man& m)
{
return m_Name == m.m_Name;
}
bool operator<(const Mans m)
{
//======= Упорядочиваем по имени
return m_Name < m.m_Name;
}
friend ostreams operator«(ostreams os, const Mans m);
};
//========= Внешняя реализация операции вывода
ostream& operator«(ostreams os, const Mans m)
{
return os « m.m_Name « ", Age: " « m.m_Age;
}
Для класса Man мы не хотим использовать class template Vector, но хотим со здать вектор объектов класса, работающий несколько иначе. С этой целью явн описываем новое конкретное воплощение (template class) класса Vector дл. типа Man.
class Vector
{
Т *data;
int size;
public:
Vector (int n, T* m);
~Vector 0 { delete [] data;
}
int Size()
{
return size;
}
T& operator [] (int i)
{
return data[i];
}
};
Vector
{
size = n;
data = new Man [n] ;
for (int i=0; i
data [i] = m[i] ;
}
Отличие от шаблона состоит в том, что конструктор класса vector
Man miles ("Miles Davis", 60); // Отдельный Man
//====== Массив объектов класса Man
Man some [ ] =
{
Man("Count Basis", 70),
Man ("Duke Ellingtcnton", 90) ,
miles,
Man("Winton Marsales", 50) ,
};
а в функцию main ( ) добавить манипуляции с реальным вектором типа Man. В коде, который приведен ниже, обратите внимание на то, что при создании вектора men из четырех объектов класса Man вторым параметром передается адрес обычного массива объектов, инициализирующий все элементы (внутреннего для шаблона) массива data:
//====== Конструируем вектор объектов
//====== на основе массива объектов
Vector
cout«"\nVector of Man: ";
//====== Вывод вектора
for (i=0; i< men. Size (); ++i)
cout « men[i] « "; ";
В шаблоне классов могут быть объявлены static данные и функции. При этом следует иметь в виду, что каждый конкретный класс, образованный по шаблону,
будет иметь свои собственные копии static членов. Эти члены будут общими для всех объектов конкретного класса, но различными для всех классов — реализаций шаблона.
При описании шаблона можно задать более одного параметра. Например:
template
Теперь при создании конкретной реализации класса можно задать размер стека
Stack
или
Stack
Важно запомнить, что числовой параметр должен быть константой. В примере переменная N могла быть описана как const int N=1024; но не могла быть переменной int N=1024;. При создании конкретного класса по шаблону возможно вложенное определение класса, например, если был описан частный случай класса — шаблон структур вида:
template
то после этого можно создать конкретную структуру, в качестве типа которой задать структуру, созданную по этому же шаблону, например:
Buffer
Между двумя закрывающими угловыми скобками » надо вставить символ пробела, так как в языке C++ операция >> имеет самостоятельный смысл, и не один. Существует возможность генерировать по шаблону классы, которые являются производными от какого-то базового класса. Например, если описать базовый класс TList, в котором не определен тип элементов хранения, то есть используется тип void, то целесообразно ввести описание шаблона производных классов:
class TList
//======== Начало списка
void *First;:
public:
void insert (void*);
int order (void*, void*, int);
//======== Другие методы
};
template
public TList T *First;
public:
void insert (T *t)
{
TList::insert(t);
}
int order (T *pl, T *p2, int n)
{
return TList::order(pi, p2, n);
}
//======= Другие методы
};
В этих условиях становится возможным декларация списка, состоящего из элементов одного определенного типа, например List
Шаблоны
Шаблоны
STL — это библиотека шаблонов. Прежде всего вспомним, что такое шаблон. Различают шаблоны функций и шаблоны классов. Шаблон функций (function template) является средством языка C++, позволяющим избежать рутинного переписывания кодов функций, которые имеют сходный алгоритм, но разные типы параметров. Классическим примером, иллюстрирующим выгоды шаблона, является множество реализаций функции max (a, b) . При отсутствии механизма шаблонов для придания функции max () универсального характера следует создать несколько функций, разделяющих одно и то же имя. Например:
long max (long a, long b);
double max (double a, double b);
MyType max (mytype a, mytype b);
Vectors max (Vectors a, Vectors b);
Очевидно, что тела всех функций могут выглядеть совершенно одинаково для многих типов параметров. Например, коды функции max могут иметь вид:
return (a>b) ? а : b;
В таких случаях удобно использовать шаблон функции. Шаблон задается ключевым словом template:
template
{
return (х>у) ? х : у;
};
Описатель
Примечание 1
Примечание 1
Не идите на поводу у ложного друга — переводчика термина operator. В английском языке он имеет смысл операции (например, операция + или операция <, операция логического или |, и т. д.). То, что мы называем оператором языка (например, оператор while, оператор for, условный оператор if, и т. д.), имеет английский аналог — statement (например, conditional statement if).
Если задан шаблон, то компилятор генерирует подходящие коды функции max () в соответствии с конкретными типами фактических параметров, использованных при вызове функции. Например, встретив во внешней функции коды:
Man a("Alex Black", 54), b("Galina Black", 44), с;
с = max (a, b);
cout « "\n Старший: " « с;
компилятор в сгенерированной по шаблону копии функции max при сравнении объектов класса Man использует функцию operator > (), которая должна быть определена внутри класса Man. Например, так:
int operator >(Man& m) { return m__Age > m. m_Age; }
Если в той же внешней функции встретится оператор:
cout « "\n max (10,011) = " « max (10,011);
то компилятор в другой копии функции max, сгенерированной по тому же шаблону, использует операцию >, определенную для стандартного типа данных int. Один раз написав шаблон функции max, мы можем вызывать ее для всех типов данных, для которых определена операция operator> (). Если для какого-то типа данных тело функции max не годится, то можно отменить (override) действие шаблона функции для этого типа. Например, определив функцию:
char* max (char* s, char *t)
{
return (strcmp (s, t) >0) ?s : t;
}
мы отменяем действие шаблона для символьных строк, так как функция, скроенная по шаблону, осуществляла бы ничего не значащее сравнение указателей s и t. При использовании шаблона следует строго соблюдать типы параметров и не надеяться на стандартные преобразования типов, по умолчанию осуществляемые компилятором при вызове обычных функций. Например, явно заданную функцию, скрывающую (отменяющую) шаблон:
double max (double, double);
можно вызывать с аргументами (int, double) или (float, long). Компилятор при этом автоматически преобразует параметры к типу double. Однако если явная декларация функции, скрывающей шаблон, отсутствует, то шаблон
template
не позволит смешивать типы при вызове функции max. Таким образом, обращение int i=max (9, 8.); вызывает сообщение об ошибке: "Could not find a match for max (int, double) ", которое означает, что не найдена функция max () для пары аргументов типа (int, double).
Стек — это несложно
Стек — это несложно
Стек — это адаптер (container adaptor), который предоставляет ограниченное подмножество всей функциональности контейнера. Термин адаптер в применении к структуре данных STL означает, что она реализована на основе какой-то другой структуры. По умолчанию стек основан на контейнере типа deque, но при объявлении можно явно указать и другой тип контейнера. Стек поддерживает вставку, удаление и инспекцию элемента, расположенного в первой (top) позиции контейнера. Стек не допускает итераций прохода по своим элементам. Говорят, что стек является структурой данных с дисциплиной доступа "last in first out" (LIFO). Вверху стека расположен элемент, который был помещен в него последним. Только он и может быть выбран в настоящий момент. При отладке следующего фрагмента не забудьте вставить директиву #include
void main()
{
//========= Создаем стек целых
stack
s.push(joy);
s.push(joe);
s.push(charlie);
//========= Проверяем очевидные вещи
assert (s.size () == 3);
assert(s.top() == Charlie);
cout « "Stack contents:\n\n";
while (s.size())
{
cout « s.top() « "; ";
//========= Уничтожает top-элемент
s.pop(); }
assert(s.empty());
}
Связыватели и адаптеры
Связыватели и адаптеры
* Связывателями (binders) называются вспомогательные шаблоны функций, которые создают некий объект (adaptor) , подстраивающий или преобразующий бинарный функциональный объект в унарный путем привязывания недостающего аргумента. Звучит запутанно, но суть достаточно проста. Представьте, что надо найти в нашей последовательности людей первого человека, который моложе, чем
Man win("Winton Kelly", 50);
Для объектов класса Man уже определена бинарная операция operator< (), которой пользуется предикат less
В этом же фрагменте мы покажем, как использовать другой адаптер mem_f un_ref, который тоже является вспомогательным шаблоном функции для вызова какой-либо функции, являющейся членом класса, в нашем случае Man. Вызов осуществляется для всех объектов класса в процессе прохода по контейнеру. Введите в состав класса Man две public-функции, выделяющие имя и фамилию человека. В коде этих функций попутно демонстрируются методы класса string, которые позволяют осуществлять поиск и выделение подстроки:
//======== Выделяем имя
Man FirstName()
{
//======== Ищем первое вхождение пробела
int pos = m_Name.find_first_of(string(" "),0);
string name = m_Name.substr(0, pos);
cout « '\n' « name;
return *this;
}
//======== Выделяем фамилию
Man SurName()
{
//======== Ищем последнее вхождение пробела
int pos = m_Name.find_last_of(" "), num = m_Name.length () - pos;
string name = m_Name.substr(pos + 1, num);
cout « '\n' « name; return *this;
}
Вектор заполняется элементами, взятыми из массива а г, и при этом используется метод assign, который стирает весь массив и вновь заполняет его элементами, копируя их из диапазона памяти, задаваемого параметрами. Далее мы показываем, как используется связыватель bind2nd и адаптер члена-функции mem_f un_ref:
void main ()
{
Man ar[] =
{
joy, joe, zoran, тагу, simon, liza, Man("Lina Groves", 19)
};
uint size = sizeof(ar)/sizeof(Man);
vector
men.assign(ar, ar+size);
pr(men,"Man Vector");
//======= Привязка второго аргумента
vector
men.end(), bind2nd(less
if (p != men.end())
cout « "\nFound a man less than " « win « "\n\t" « *p;
//======= Использование метода класса (mem_fun_ref)
cout « "\n\nMen Names:\n";
for_each (men.begin(), men.end(), mem_fun_ref(&Man::SurName));
cout « "\n\nMen First Names:\n";
for_each (men.begin (), men.end(), mem_fun_ref(&Man::FirstName));
cout « "\n\n";
}
Напомним, что для успешной работы вы должны вставить в функцию main тот набор объектов класса Man, который был приведен ранее.
Примечание 1
Примечание 1
При анализе этого кода бросается в глаза неестественность прототипов функций SurName и FirstName. Логика использования этих функций совсем не требует возвращать какое-либо значение, будь то Man, или переменная любого другого типа. Естественным выбором будет прототип void SurNameQ;. Но, к сожалению, этот выбор не проходит по неизвестным мне причинам ни в Visual Studio б, ни в Studio.Net 7.O. Я достаточно много времени потратил на бесполезные поиски ответа на этот вопрос и пришел к выводу, что это ошибка разработчиков. В подтверждение такого вывода приведу следующие аргументы. Во-первых, измените тип возвращаемого значения на любой другой, но не void, и программа будет работать. Например, возьмите прототип string SurName(); и возвращайте return "MicrosoftisOK"; (или другую пару: int и-127). Во-вторых, все примеры на (mem_fun_ref) в документации MSDN возвращают загадочный bool. В-третьих, в документации SGI (Silicon Graphics) приведены аналогичные примеры с функциями, возвращающими void. Там, как вы знаете, используется другая платформа (IRIS). В-четвертых, наш пример (без void) проходит в Visual Studio б и не работает в бета-версии Studio.Net. Будем надеяться, что ситуация со временем выправится.
Адаптер mem_fun в отличие от mem_fun__ref используется с контейнерами, хранящими указатели на объекты, а не сами объекты. Хорошим примером использования mem_f un, в котором иллюстрируется полиморфизм позднего связывания (late binding polymorphism), является следующий:
//======== Базовый класс. К сожалению, абстрактным
//======= его не позволит сделать контейнер
struct Stud
virtual bool print()
{
cout « "\nl'm a Stud";
return true;
}
};
//===== Производный класс struct GoodStud : public Stud
{
bool print ()
{
cout « "\nl'm a Good Stud";
return true;
}
};
//======= Еще один производный класс
struct BadStud : public Stud
{
bool print ()
{
cout « "XnI'm a Bad Stud";
return true;
}
};
//======= Иллюстрируем полиморфизм в действии
void main () {
//====== Вектор указателей типа Stud*
vector
//====== Они могут указывать и на детей
v.push_back (new StudO);
v.push_back (new GoodStudO);
v.push_back(new BadStud(J);
//====== Выбор тела метода происходит поздно
//====== на этапе выполнения
for_each(v.begin(), v.end(), mem_fun(&Stud:: print));
cout <<"\n\n";
}
Конечно же, эта программа выведет:
I'm a Stud
I'm a Good Stud
I'm a Bad Stud
так как mem_fun будет вызвана с помощью указателя типа stud* (на базовый класс) — непременное условие проявления полиморфизма, то есть выбора конкретной версии виртуальной функции (адреса функции из vtable) на этапе выполнения. Выбор определяется конкретной ситуацией — типом объекта, попавшим под родительский перст (указатель) в данный момент времени.
Решаем краевую задачу
Диалог для исследования решений
Диалог для исследования решений
Сейчас мы быстрыми темпами, не углубляясь в детали, создадим диалог, работающий в немодальном режиме и позволяющий исследовать решения уравнения Пуассона при разных значениях свойств среды, произвольном расположении источников поля и произвольных граничных условиях.
Так как диалог будет вызываться по команде меню, откройте в окне редактора ресурс меню IDR_MAINFRAME и приведите его в соответствие со следующей схемой. В меню File должна быть только одна команда Exit, в меню Edit уберите все команды и вставьте одну команду Parameters, индекс (ID_EDIT_PARAMETERS) ей будет присвоен автоматически. Остальные меню оставьте без изменения. С помощью редактора диалогов создайте новое диалоговое окно (форму), которое имеет вид, изображенный на Рисунок 11.4. Типы элементов управления, размещенных в окне диалога, и их идентификаторы сведены в табл. 11.1. Затем создайте класс для управления диалогом.
Форма диалога для управления параметрами краевой задачи
Рисунок 11.4. Форма диалога для управления параметрами краевой задачи 
Учитывая сказанное, создадим программный модуль,
Учитывая сказанное, создадим программный модуль, который позволяет проверить наши возможности управления последовательностью valarray на примере задачи, близкой к реальности. Самым сложным моментом в реализуемом плане является создание функции f (), с помощью которой генерируется матрица заданной структуры, но произвольной размерности п. При генерации она помещается в последовательность типа valarray. Вторая функция (f и) проста. С ее помощью вычисляются коэффициенты уже известного вектора решений1:
//====== Глобально заданная размерность системы
int n;
//====== Граничные условия
double UO, UN;
//====== Функция вычисления коэффициентов
//====== трехдиагональной матрицы
double f ()
{
//====== Разовые начальные установки
static int raw = -1, k = -1, col = 0;
//====== Сдвигаемся по столбцам
col++;
//====== k считает все элементы
//====== Если начинается новая строка
if (++k % n == 0)
{
col =0; // Обнуляем столбец
raw++; // Сдвигаемся по строкам
}
//====== Выделяем три диагонали
return col==raw ? -2.
: col == raw-1 И col==raw+l ? 1.
: 0.;
}
double fu()
{
//==== Вычисления вектора правых частей по формуле (5)
static double
dU = (UN-UO)/(n+1),
d = U0; return d += dU;
}
В функции main создается valarray с трехдиагональной матрицей и производится умножение матрицы на вектор решений. Алгоритм умножения использует сечение, которое вырезает из valarray текущую строку матрицы:
void main()
{
//======= Размерность задачи и граничные условия
n =4;
UO = 100.;
UN = 0 . ;
//======= Размерность valarray (вся матрица)
int nn = n*n;
//======= Матрица и два вектора
valarray
//======= Генерируем их значения
generate (&а[0], &a[nn], f); generate (&u[0], &u[n], fu);
out ("Initial matrix", a); out ("Initial vector", u);
//======= Умножение матрицы на вектор
for (int i=0; i
//======= Выбираем i-ю строку матрицы
valarray