Учебник по Visual C++ .Net
Классы приложения Раскройте элемент
CMyView::OnDraw(CDC* pDC){
CMyDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
}
Здесь вместо подсказки // TODO: мы должны вставить код, отображающий данные документа. Функция OnDraw(CDC *pDC) входит в состав класса CMyView, являсь методом этого класса, и вызывается каркасом приложения в тех случаях, когда необходимо перерисовать окно, обслуживаемое классом CMyView.
Представлением документа называется клиентская область одного из окон-рамок, обслуживаемых классом CChildFrame и живущих внутри главного окна приложения. В MDI приложении их может быть много. Это те окна, которые можно видеть по очереди, все сразу, каскадом или рядом, не перекрывая друг друга (Cascade или Tile).
Перед тем как начать отображение данных, надо эти данные создать или добыть из класса, обслуживающего документ. В соответствии с концепцией архитектуры «документ — представление» все стратегические данные приложения должны храниться в классе документа, то есть в классе CMyDoc. Метод GetDocument (он вызывается в заготовке OnDraw) класса CMyView позволяет добыть указатель на объект класса CMyDoc, который управляет активным в данный момент документом.
Итак, имея адрес документа, мы можем начинать отображение его данных в окне представления. В системе, поддерживающей графический интерфейс пользователя, все данные не просто выводятся на экран, они скорее «рисуются» в контексте устройства, связанном с окном. Подсистема Windows GDI (Graphics Device Interface) дает вам набор средств для рисования, среди которых одним из главных является контекст устройства, управляемый классом CDC (Device Context). Указатель на используемый системой в данный момент объект класса CDC мы получили в качестве параметра функции OnDraw.
Концепция такова: рисование производится в специальной области памяти, управляемой этим классом, с помощью графических примитивов (точек, прямоугольников и т. д.) и логической системы координат. Далее Windows производит преобразование логических координат примитивов в систему физических или аппаратных (device) координат. Идея в том, что программист отображает данные в контексте устройства, не задумываясь о том, где они будут реально воспроизведены (на экране хорошего или плохого монитора, на принтере или на графопостроителе). Программист стремится наиболее точно отобразить данные документа в произвольно выбранной им логической системе координат, а система с помощью драйверов устройств стремится наиболее точно преобразовать все точки рисунка в физическую или аппаратную, то есть связанную с конкретным устройством, систему координат.
Концепция решений и проектов
Сеанс работы в Studio.Net начинается с открытия существующего или создания нового решения (solution). В дальнейшем вместо термина решение я иногда буду использовать термин рабочее пространство, так как буквальный перевод — решение — не всегда точен. Файлы с расширением sin используются IDE (Integrated Development Environment) для хранения настроек и начальных установок конкретных решений. Концепция решений помогает объединить проекты и другие элементы в одном рабочем пространстве. Множество файлов разного типа, в рамках одного решения составляют приложение (application) Visual Studio.Net 7.0. Рабочее пространство может содержать несколько проектов, быть пустым или содержать файлы, которые имеют смысл и вне контекста решений. В любом случае, вы должны начинать работу в студии с открытия существующего или создания нового рабочего пространства.Проект как часть решения состоит из отдельных компонентов, например файлов, описывающих форму окна или шаблон диалога (re-файл), файлов с исходными кодами программных модулей (.срр, .cs) и/или файлов, представляющих собой описание запроса к базе данных (database script), HTML-документов и, т. д. Настройки проектов хранятся в специальных файлах проектов. Они могут иметь разные расширения, так как в одном пространстве можно объединять проекты совершенно разных типов. Например, проект MFC-приложения хранит свои установки в файле с расширением vcproj, а файл проекта, реализованного на языке
С#, имеет расширение csproj. Такой файл является читаемым, его можно открыть вне рамок Studio.Net (например, с помощью Notepad) и увидеть описание установок проекта на еще одном из «секретных» языков. Например, проект типа MFC Application с именем MyProj содержит файл MyProj.vcproj, начальный фрагмент которого мы приведем здесь:
Name="Debug|Win32"
InterraediateDirectory="Debug"
OutputDirectory="Debug"
ConfigurationType="l"
UseOfMFC="2"
CharacterSet="2">
Name="VCCLCorapilerTool"
Optimization="0"
Нет необходимости углубляться в анализ языка описания проекта. Поверхностного взгляда достаточно, чтобы понять, что мы имеем дело с последовательностью <предложений>, описывающих тип проекта, настройки и перечень инструментов Studio.Net для его обработки. То же самое можно сказать про sin-файл. Он читаем, и если открыть его в текстовом режиме, то можно увидеть предложения некоего служебного языка, описывающие состав и настройки рабочего пространства.
Контейнер точек Зададимся целью
Окно мастера Add VariableПросмотрите описание класса CMyDoc, дважды щелкнув на имени класса в окне Class View. В конце файла вы должны увидеть строку
vector
Теперь просмотрите тело конструктора класса. Для этого раскройте элемент дерева CMyDoc и дважды щелкните на имени конструктора CMyDoc (void). Вы должны увидеть такой заголовок и тело конструктора:
CMyDoc::CMyDoc()
: m Points (0)
{
}
Обратите внимание на инициализатор m_Points (0), который был автоматически вставлен мастером Add Variable. Инициализатор вызывает один из конструкторов шаблона классов vector и сообщает ему, что перед тем, как создать объект класса CMyDoc, надо создать объект m_Points типа vector и задать ему нулевой размер. Нам не нужен этот инициализатор, так как мы собираемся записать в контейнер m_Points координаты тестового многоугольника. Тело конструктора документа пока пусто. Наполним его кодами, вычисляющими точки многоугольника, так чтобы он имел вид пятиконечной звезды. Звезда удобна тем, что позволяет продемонстрировать способы закраски самопересекающихся многоугольников. Измените коды конструктора:
CMyDoc: : CMyDoc ()
//====== Вспомогательные переменные
double pi = 4 . * atari (1.),
al = pi / 10. , // Углы
a2 = 3. * al,
// ====== 2 характерные точки
x1 = cos (al) ,
yl = sin (al) ,
x2 = cos (a2) ,
y2 = sin(a2) ,
x[5], у [5];
//===== Вещественные (World) координаты углов звезды
//===== Считаем, что начало координат находится
//===== в геометрическом центре звезды
х [ 0 ] = 0 . ; у [ 0 ] = 1 . ; // Макушка звезды
х[1] = -х2; у[1] = -у2; // Нижний левый угол
х[2] = xl; У [2] = yl; // Верхний правый угол
х[3] = -xl; y[3] = yl; // Верхний левый угол
х[4] = х2; У [4] = -у2; // Нижний правый угол
//===== Логические координаты углов звезды
//===== запоминаем в контейнере
for (int i=0; i<5; i++)
//===== Точка в логической системе координат
// Увеличиваем в 100 раз, переводим в целые
// и сдвигаем
CPoint pt(200 + int(100. * x[i]), 150 - int(100. * y[i]));
//===== Записываем в конец контейнера
m_Points.push_back(pt);
}
}
Начало работы с Visual Studio.Net
Итак, вы успешно преодолели все трудности установки Microsoft Visual Studio. Net 7.0 (если они были, а они в изобилии присутствовали в бета-версии продукта, с которой я имел дело в момент написания книги) и готовы покорить определенные высоты с помощью вашей неудержимой фантазии программиста и возможностей студии. Инструменты Studio.Net, несомненно, помогут воплотить ваши идеи в реальные проекты, которые теперь принято называть решениями (solutions) — термин, обозначающий новую концепцию логического хранилища проектов.
Если вы имеете опыт работы в среде Microsoft Visual Studio 6.0, то, открыв Studio.Net, вы сразу отметите значительные изменения в интерфейсе. Общий облик вызывает ассоциации с пультом управления летательного аппарата или какого-то другого сложного технического объекта. Задача одна — в небольшом пространстве разместить множество инструментов контроля и управления за состоянием объекта. Но в отличие от осязаемого пульта управления самолетом ваш иллюзорный пульт на экране может динамично изменяться, отчасти благодаря сравнительно новым элементам управления — tabbed windows — окнам с вкладками. Открыв Studio.Net, вы увидите такое окно на самом видном месте. Это окно самое большое по площади. В начальный момент оно имеет только одну страницу (page), открываемую с помощью вкладки (tab) VS Home Page. Далее в этой группе будут появляться другие вкладки, позволяющие открывать другие страницы составного окна. В случае если вы «потеряете» начальное окно, то его можно вернуть на свое место, дав команду View > Web Browser. Вот другой способ сделать это:
При поиске кнопок используйте всплывающие подсказки (tooltips).
Обозревая окна Studio.Net, отметьте усовершенствования косметического характера: пункты меню теперь имеют значки, изменились цвета элементов интерфейса в разных состояниях, нарушив тем самым рекомендации Microsoft по созданию UI-элементов (User Interface).

Рис. 1.1. Общий вид Studio.Net
Реакция на ошибки Полезно откомпилировать
Окно Task List со списком ошибокВоспользуйтесь контекстным меню окна Task List и выберите команду Sort by > Category. В результате ее выполнения на первое место попадает сообщение:
error C2238: unexpected token (s) preceding ";" ...
Это примерно значит: «Неизвестная лексема предшествует точке с запятой». Такое сообщение, на мой взгляд, значительно более точно определяет причину. Вот она: компилятор не знает, что такое vectoro. Причина: мы забыли подключить файл с заголовками библиотеки STL Это легко исправить, вставив в нужное место строки:
//====== Подключает часть определений STL
#include
//====== Задает область видимости имен STL
using namespace std;
Теперь надо решить, куда вставить эти строки. Их можно вставить в начало файла MyDoc.h, по это будет расточительно. Дело в том, что для подключения файлов заголовков различных библиотек существует специальное место — файл Stdafx.h. Этот файл (совместно с файлом StdAfx.cpp) используется для построения файла скомпилированных заголовков My.pch (precompiled header) и файла скомпилированных типов StdAfx.obj.
Они расположены в папке Debug вашего проекта и служат для ускорения повторных компиляций файлов проекта после внесенных вами изменений, если они незначительны. Таким образом, подключаемые файлы библиотек, а они внушительны по размерам и компилируются только при необходимости. Сейчас настала такая необходимость, так как исправления затрагивают файл Stdafx.h. Вставьте две вышеуказанные строки в конец файла Stdafx.h и запустите музыку (Ctrl+F5).
Отметьте, что все файлы проекта компилируются заново, так как все файлы типа .срр имеют в качестве первой строку:
#include "stdafx.h"
Мы изменили stdafx.h, и компилятор заново проходит по всем зависящим (dependent) от него файлам. После построения и запуска изображение звезды должно появиться в клиентской области дочернего окна-рамки, то есть в окне, управляемом классом CMyView.

Рис. 1.7. Окно приложения My
Обратите внимание на характер заливки внутренних частей полигона, который принят по умолчанию. Он идентифицируется символьной константой ALTERNATING, но есть еще один вариант заливки — WINDING. Вставьте в функцию OnDraw, перед выводом полигона, строку.
pDC->SetPolyFillMode(WINDING);
и нажмите Ctrl+F5. Характер заливки изменился. Объяснение этого факта (и многих других) надо научиться искать в документации, сопровождающей Studio.Net. Дайте команду Help > Index, в окно Look for введите SetPolyFillMode и нажмите Enter. Появится окно Index Results for..., в котором следует сделать выбор между API-функцией SetPolyFillMode и одноименным методом класса CDC. Так как мы работаем с библиотекой MFC, то выбор почти всегда падает на методы классов, а не на одноименные функции API. Текст справки появится в окне Web Browser (многовато окон), и если вы действительно хотите понять алгоритм закрашивания кистью внутренних частей полигона, то вам придется немного потрудиться, даже имея хороший английский. К таким ситуациям тоже надо выработать правильное отношение. Программист должен быть кропотлив и терпелив.
Подведем итог:
Рисование в контексте устройства
CMyView::OnDraw(CDC* pDC){
CMyDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc) ;
//======= Узнаем размер контейнера точек
UINT nPoints = pDoc->m_Points.size() ;
//======= Уходим, если он пуст
if (InPoints)
return;
//=== Сохраняем текущее состояние контекста
//=== (инструменты GDI)
pDC->SaveDC () ;
//=== Создаем перо Windows для прорисовки контура
CPen pen (PS_SOLID,2,RGB(0,96,0));
//=== Выбираем его в контекст устройства
pDC->SelectObject (Spen);
//===Создаем кисть Windows для закраски внутренности
CBrush brush (RGB(240,255,250));
pDC->SelectObject (&brush);
//===== Изображаем полигон
pDC->Polygon (spDoc->m_Poihts[0], nPoints);
//Восстанавливаем контекст (предыдущие инструменты GDI)
pDC->RestoreDC(-l);
}
Создание нового проекта При создании
Окно диалога New Project
Рис. 1.3. Окно мастера MFC Application Wizard
Мы отложим разговор о различных типах шаблонов (стартовых заготовках) MFC-приложений в предположении, что читатель имеет представление о них по опыту работы в Visual Studio б.0. Если это не так, то все равно не прерывайте процесс чтения и исследования Studio.Net. Примиритесь, временно, с дискомфортом недопонимания. Итак, Application Wizard потрудился и создал стартовую заготовку нового Windows-приложения, которое поддерживает многодокументный интерфейс (MDI). Вы можете немедленно его запустить, дав команду Debug > Start Without Debugging и согласившись с сообщением о том, что конфигурация проекта устарела (подразумевается, ехе-файл либо отсутствует, либо старше, чем какой-либо из исходных файлов проекта). После этого вы имеете возможность наблюдать за процессом компиляции и компоновки в окне Output, которое, скорее всего, появится внизу главного окна Studio.Net. Далее вы увидите окно нового приложения My, поддерживающего стандарт MDI. Опробуйте команды File > New и все команды меню Window этого приложения, следя за заголовками новых дочерних окоп. Закройте стартовое приложение и сосредоточьте внимание на окне Studio.Net с заголовком Solution Explorer, которое, скорее всего, расположено справа от окна VS Home Page.
Опробуйте также команды меню Window. Например, выберите произвольное окно и дайте команду Window > Docable. Отыщите выбранное окно, затем повторите ту же команду и вновь отыщите окно. При работе с кодами в окне редактора наиболее удобным режимом для вспомогательных окон представляется Auto Hide. Studio.Net позволяет очень гибко управлять местоположением своих окон. Отметьте, что вышеупомянутое контекстное меню динамически изменяется в зависимости от текущего состава группы tabbed-окон. В нем появляются другие команды, которые расширяют ваши возможности по управлению интерфейсом Studio.Net. Окно Solution Explorer дает возможность управлять проектами и файлами проектов. Оно является аналогом окна File View в Visual Studio 6 и теперь по умолчанию входит в группу окон, составляющих блок очень полезных страниц (pages), которыми вы будете часто пользоваться. Сейчас активизируйте страницу Class View из этого блока, для того чтобы увидеть состав классов библиотеки MFC, использованный в новом приложении. При поиске вкладки Class View в блоке используйте всплывающие подсказки. Раскройте дерево классов. Отметьте, что теперь кроме шести классов(CAboutDlg, CChildFrame, CMainFrame, CMyApp, CMyDoc, CMyView)
в дерево входят и другие элементы, также являющиеся логическими компонентами MFC-приложения. Это глобальные функции и переменные (Global Functions and Variables), макроподстановки И константы (Macros and Constants).

Рис. 1.4. Представление классов в окне Class View
Учебник по Visual C++ .Net
Масштабирование изображения
Зададимся целью внести изменения в приложение My, которое мы создали в предыдущей главе, таким образом, чтобы изображение геометрической фигуры всегда было в центре окна и следило за изменением размеров окна приложения, меняясь пропорционально. Напомним, что фигурой является многоугольник с произвольным количеством вершин, и он выводится в контекст устройства Windows с помощью объекта класса CDC. Для того чтобы упростить процесс слежения за размерами окна представления, введем в число членов класса CMyView новую переменную, которая будет хранить текущие размеры окна.Чтобы реализовать указанный подход, надо заменить в классе документа массив целых координат на массив вещественных, а в классе CMyView создать еще один массив, но уже целых координат. Используя уже знакомую технику, введите в состав класса CMyView private-переменную
vector
Ее имя совпадает с именем массива координат в документе, но это не помеха, если используешь ООП. Классы ограничивают область действия имен, скрывая их. В интерфейсе класса документа (файл MyDoc.h) замените объявление
vector
на
VECPTS m_Points;
Этой заменой мы оставили для контейнера то же имя, но изменили тип его элементов. Тип данных VECPTS — вектор точек с вещественными (World) координатами — пока не определен, но мы собираемся его декларировать и определить для того, чтобы было удобно хранить реальные координаты объекта. Для начала создадим свой собственный класс CDPoint, инкапсулирующий функциональность точки с вещественными координатами. Вставьте в начало файла MyDoc.h после директивы препроцессора #pragma once, но до объявления класса CMyDoc декларацию нового класса1:
//====== Новый класс "Вещественная точка"
class CDPoint
{
public:
double x;
double у; // Вещественные координаты
//====== Конструктор по умолчанию
CDPoint()
{
х=0.;
у=0.;
}
//====== Конструктор копирования
CDPoint(const CDPointS pt)
{
x = pt.x;
y = pt.y;
}
//====== Конструктор с параметрами
CDPoint(double xx, double yy)
{
x = x x;
у = yy;
}
//====== Операция умножения (увеличение в п раз)
CDPoint operator*(UINT n)
{
return CDPoint (x*n, y*n);
}
//====== Операция присваивания
CDPointS operator=(const CDPointS pt)
{
x = pt.x;
у = pt.y;
return *this; // Возвращаем свой объект
}
//====== Операция сложения двух точек
CDPoint operator*(CDPointS pt)
{
return CDPoint(x + pt.x, у + pt.y);
}
//====== Операция вычитания двух точек
CDPoint operator-(CDPointS pt)
{
return CDPoint(x - pt.x, у - pt.y);
}
// Метод приведения к типу CPoint (целая точка)
CPoint Tolnt()
{
return CPoint(int(x),int(у)); }
//====== Операция сложения с записью результата
void operator+=(CDPointS pt) { x += pt.x; у += pt.y; }
//====== Операция вычитания с записью результата
void operator-=(CDPoint& pt) { x — pt.x; у -= pt.y; }
// Операция вычисления нормы вектора, заданного точкой
double operator!() { return fabs(x) + fabs(y); } };
При использовании контейнера объектов класса полезно декларировать новый тип данных:
typedef vector
Вставьте эту строку сразу после объявления класса CDPoint. Данное объявление позволяет просто создавать множество контейнеров для хранения точек с вещественными координатами. Представим, что документ должен хранить контуры нескольких деталей сложной конструкции. Тогда каждая деталь конструкции может быть объявлена объектом типа VECPTS, то есть она является контейнером, содержащим координаты точек своего контура.
Кроме трех конструкторов для удобства пользования в классе CDPoint заданы правила сложения и вычитания точек. Метод Tolnt позволяет создать стандартную точку Windows (CPoint) из нашей вещественной точки. Операция умножения точки на целое число без знака (CDPoint operator* (UINT n);) позволяет увеличивать координаты объекта, что мы используем при переходе из World- в Page-пространство. Операция вычисления нормы вектора, начало которого находится в точке (0,0), а конец в данной точке, полезна при оценке степени близости двух точек. В одном из следующих уроков она нам пригодится. Тело конструктора документа можно упростить, так как теперь он помнит реальные координаты объекта и необходимость преобразовывать координаты в пространство Page исчезла. Это преобразование мы сделаем в классе CMyView:
CMyDoc::CMyDoc()
{
//====== Вспомогательные переменные
double pi = 4. * atan(l.), //====== Углы
al = pi / 10., a2 = 3. * a1,
//===== 2 характерные точки
xl = cos(al), yl = sin(al), x2 = cos(a2), y2 = sin(a2);
//==== Вещественные (World) координаты углов звезды
m_Points.push_back(CDPoint( 0., 1.) ) ;
m_Points.push_back(CDPoint(-x2, -y2));
m_Points.push_back(CDPoint( xl, yl)) ;
m_Points.push_back(CDPoint(-xl, yl));
m_Points.push_back(CDPoint( x2, -y2));
}
Масштабирование изображения можно упростить, если следить за текущими размерами клиентской области окна. При изменении пользователем размеров окна-рамки в момент отпускания кнопки мыши система посылает приложению сообщение WM_SIZE, на которое должен среагировать класс CMyView и запомнить в переменной m_szView новые размеры.
Сейчас мы введем в состав CMyView новую функцию отклика, которая будет вызываться в ответ на приход сообщения WM_SIZE. Она должна иметь имя OnSize (так устроена MFC) и иметь особый спецификатор afx_msg, который невидим компилятором (препроцессор заменит его пустым местом), но нужен инструментам Studio.Net. Спецификатор несет информацию о том, что функция OnSize особая — она является эффективно реализованным обработчиком сообщения (message handler). В Studio.Net процесс создания функций-обработчиков и виртуальных функций сильно изменен. Теперь это делается не с помощью ее инструмента ClassWizard, следы которого однако присутствуют в студии, а в окне Properties.
void CMyView::OnSize(UINT nType, int ex, int cy)
{
//========== Вызов родительской версии
CView::OnSize(nType, ex, cy) ;
if (cx==0 cy==0)
return;
//========= Запоминаем размеры окна представления
m_szView = CSize (ex, су); } ;
Проверка if (cx==0...) необходима потому, что каркас приложения вызывает OnSize несколько раз и иногда с нулевыми размерами. Обратите внимание на то, что мастер вставок добавил также и прототип (объявление) функции обработки в интерфейс класса CMyView (см. файл MyView.h):
public:
afx_msg void OnSize(UINT nType, int ex, int cy) ;
Теперь покажем, как с помощью Studio.Net следует определять в классе собственные версии виртуальных функций. Мы собираемся однократно (при открытии окна) преобразовать «мировые» координаты в логические и запомнить их. Это удобно сделать внутри виртуальной функции OnlnitialUpdate, которая унаследована от класса cview. Она вызывается каркасом приложения в тот момент, когда окно еще не появилось на экране, но уже существует его Windows-описатель (HWND) и объект класса CMyView прикреплен (attached) к окну. Напомним также, что документ имеет и поддерживает динамический список всех своих представлений.
Результат ищите в конце файла MyView.cpp. Внесите изменения в тело функции:
void CMyView::OnlnitialUpdate()
{
CView::OnlnitialUpdate();
// Создаем ссылку на контейнер World-координат точек
VECPTSS pts = GetDocument()->m_Points;
UINT size = pts.size ();
//====== Задаем размер контейнера логических точек
m_Points. resize (size);
for (UINT i=0; i < size;
m_Points[i] = (pts[i] * m_nLogZoom) .Tolnt () ;
}
Здесь мы добываем из документа World-координаты объекта, умножаем их на коэффициент m_nLogZoom и преобразуем к целому типу. Обратите внимание на использование операций и методов вновь созданного класса CDPoint и на то, что переменная pts создана и инициализирована как ссылка на контейнер точек документа. Теперь осталось изменить коды для перерисовки представления так, чтобы воспользоваться техникой масштабирования, которую мы обсудили в начале главы:
void CMyView: :OnDraw(CDC* pDC)
{
CMyDoc* pDoc = GetDocument () ;
ASSERT_VALID(pDoc) ;
//====== Узнаем размер контейнера точек
UINT nPoints = m_Points.size () ;
if (! nPoints) // Уходим, если он пуст return;
// Сохраняем текущее состояние контекста pDC->SaveDC() ;
// Создаем перо Windows для прорисовки контура
CPen pen (PS_SOLID,2,RGB(0, 96,0) ) ;
//====== Выбираем его в контекст устройства
pDC->SelectObject (spen);
// Создаем кисть Windows для заливки внутренности
CBrush brush (RGB (240, 255, 250) );
pDC->SelectObject (Sbrush);
//====== Задаем режим преобразования координат
pDC->SetMapMode(MM_ISOTROPIC) ;
//====== Сдвиг в логической системе
pDC->SetWindowOrg(0,0) ;
//====== Сдвиг в физической системе
pDC->SetViewportOrg (m_szView.cx/2, m_szView. су/2) ;
//====== Знаменатель коэффициента растяжения
pDC->SetWindowExt (3*m_nLogZoom, 3*m_nLogZoora) ;
//====== Числитель коэффициента растяжения
pDC->SetViewportExt (m_szView.cx, -m_szView.cy) ;
//====== Изображаем полигон
pDC->Polygon (Sra_Points [0] , nPoints) ;
// Восстанавливаем контекст (предыдущие инструменты GDI)
pDC->RestoreDC (-1) ;
}
Коэф(рициент 3 в параметрах SetWindowExt моделирует ситуацию, когда лист ватмана в 3 раза превышает размер детали, на нем изображенной. Знак минус в параметре SetViewportExt позволяет компенсировать изменение направления оси Y при переходе из Page space в Device space. При рисовании мы используем логические (Page) координаты, которые хранятся в классе CMyView.

Рис. 2.1. Изображение объекта в режиме MMJSOTROPIC

Рис. 2.2. Изображение объекта в режиме MM_ANISOTROPIC
Запустите приложение (Ctrl+F5) и попробуйте изменять размеры окна, по отдельности сжимая его до предела вдоль обеих координат. Наблюдайте за тем, как размеры звезды следят за размерами окна. Отметьте, что фигура остается в центре окна, а ее пропорции остаются без изменений. Затем задайте режим отображения MM_ANISITROPIC и вновь запустите приложение. Проведите те же манипуляции с окном и отметьте, что пропорции объекта теперь искажаются. Тем не менее оба режима используются на практике. Например, нет смысла изменять пропорции какой-либо конструкции или ее детали, но вполне допустимо изменять пропорции графика функции.
Анализируя трансформации звезды, имейте в виду, что ее размах 2 абстрактные, не определенные нами единицы в world-пространстве и 200 логических единиц в Page-пространстве. Само Page-пространство мы задали с помощью выражения 3*m_nLogZoom, то есть 300 на 300 единиц. При переходе к Device-координатам система растягивает или сжимает его так, чтобы оно занимало все окно. При этом она сдвигает изображение и центр звезды (0,0) попадает в центр окна (m_szview. сх/2, m_szView. су/2). Полезно поэкспериментировать и с другими режимами преобразования координат. При этом следует помнить, что все они, кроме режима ММ_ТЕХТ, предполагают ось Y направленной вверх.
Режимы отображения координат
В Windows любые операции вывода являются графическими. Подсистема GDI делает вывод анпаратно независимым. Это означает, что информация, выводимая на любое физическое устройство, будет выглядеть почти одинаково. На экране монитора, на плоттере и на принтере будут отражены все детали текста или изображения. Кроме того, GDI поддерживает логические устройства вывода, такие как память или диск. Для осуществления вывода в среде Windows необходимо сначала создать так называемый контекст устройства (device context). Контекст устройства — это объект, определенный в GDI, который содержит содержат подробную информацию об устройстве, куда предполагается направить графический вывод. Например: основной цвет и цвет фона, используемая палитра, шрифт по умолчанию и т. д.
MFC имеет специальный набор классов, упрощающий процедуру общения с контекстом устройства. Класс CDC содержит большую часть функций, которые могут понадобиться для управления выводом. Классы, производные от CDC, обеспечивают специальные возможности, например класс cciientoc обеспечивает доступ к клиентской области окна, где в основном разворачиваются события, управляемые программистом. Класс CPaintDC позволяет управлять процессом перерисовки окон, обеспечивая вызовы функций BeginPaint и EndPaint в ответ на сообщение WM_PAINT. Создание рисунка в окне производится с помощью функций API, инкапсулированных в одноименных методах класса CDC
Если мы зададимся целью масштабировать изображение, то есть дать пилы-юним-;-лю возможность изменять его размеры, то нам следует научиться управлять режимами и параметрами отображения логической системы координат в физическую. Вы уже знаете, что Win32 API использует четыре координатных пространства (spaces): World space, Page space, Device space, и Physical Device space. Однако большая часть документации и книг но MFC оперируют только двумя терминами: логическая и физическая системы координат. Мы будем использовать термин, логическая система координат, имея в виду Page space.
Заметьте, что и World space и Page space измеряют плоскую область, размах которой по обеим координатам равняется 232 логических единиц, то есть более 4 миллиардов единиц. Page space работает совместно с Device space, чтобы обеспечить приложение единицами, не зависящими от типа физического устройства, такими как миллиметры и дюймы-(inches). Конечным координатным пространством, Physical Device space обычно является клиентская область окна приложения, или весь экран монитора, или страница бумаги принтера (плоттера). Размеры области физического устройства изменяются в зависимости от марки, технологии и т. д. Чтобы верно передать детали изображения, созданного в логической системе, в физическое устройство, система преобразовывает их путем отображения (mapping) прямоугольной области из одного координатного пространства в другое. При копировании каждой точки прямоугольной области из одного пространства в другое Windows применяет алгоритм трансформации, который в конечном счете изменяет размеры, ориентацию и форму всего объекта.
Так как рисование производится в логической структуре, называемой контекстом устройства, то рисунок не зависит от конкретного устройства, в которое будет осуществлен вывод изображения. Функциональность контекста устройства поддерживается классом CDC или одним из его потомков. Для выбора режима отображения используются методы этого класса:
Восемь существующих режимов отображения координат задаются с помощью символьных констант, определенных в файле Wingdi.h:
#define MM_TEXT 1 // 1 лог. ед. - 1 пиксел
#define MM_LOMETRIC 2 // 1 лог. ед. - 0,1 мм
#define MM_HIMETRIC 3 // 1 лог. ед. - 0,01 мм
#define MM_LOENGLISH 4 // 1 лог. ед. - 0,01 дюйма
#define MM_HIENGLISH 5 // 1 лог. ед. - 0,001 дюйма
#define MMJTWIPS 6 //1 лог. ед. - 1/1440 дюйма
//========== Преобразования по формуле ==========//
#define MM_ISOTROPIC 7 // Растяжение одинаково
#define MM_ANISOTROPIC 8 // Растяжение различно
По умолчанию действует режим ММ_ТЕХТ, в котором ось Y имеет направление сверху вниз. Последующие пять режимов предполагают, что ось Y направлена снизу-вверх. В двух последних режимах преобразование координат из логической системы в физическую выполняется в соответствии с формулами, которые приведены ниже. При этом используются следующие обозначения:
Выделенные точки window и viewport совмещаются, а изображение растягивается или сжимается так, что горизонтальная координата каждой точки изображения при выводе в физическое устройство вычисляется по формуле линейного преобразования:
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-арифметикой).
Учебник по Visual C++ .Net
Анализ стартовой заготовки Первые
WIN32_LEAN_AND_MEANуменьшает размер исполняемого модуля, так как исключает те фрагменты каркаса приложения, которые редко используются. Второй файл (API.h) создал мастер. Вы можете открыть его с помощью окна Solution Explorer и увидеть, что он содержит лишь две строки:
#pragma once
#include"resource.h"
Директива fpragma once сообщает компилятору, что данный файл (API.h) должен быть использован при построении кода приложения только один раз, несмотря на возможные повторы (вида #include "API.h"). Вторая директива подключает файл с идентификаторами ресурсов. Сами ресурсы вы видите в окне Resource View. Все ресурсы приложения и их отдельные элементы должны быть идентифицированы, то есть пронумерованы. Новичкам рекомендуется открыть файл resource.h с помощью окна Solution Explorer и просмотреть его содержимое. В этом файле символическим именам (идентификаторам) IDS_APP_TITLE, IDR_MAINFRAME и т. д. соответствуют числовые константы, которые препроцессор вставит вместо идентификаторов еще до процесса компиляции. В конце файла содержатся пометки Studio.Net, определяющие дальнейший способ нумерации ресурсов различного типа. Рассматриваемый файл не рекомендуется редактировать вручную, так как в случае ошибок вы получите труднолокализуемые отказы. Studio.Net сама следит за состоянием resource.h, вставляя и удаляя макроподстановки #def ine по мере того, как вы редактируете ресурсы с помощью специальных редакторов.
Возвращаясь к коду заготовки, отметим, что далее следует объявление глобальных переменных
HINSTANCE hlnst; // Текущий экземпляр
TCHAR szTitle[MAX_LOADSTRING];
// Текст заголовка окна
TCHAR szWindowClass[MAX_LOADSTRING];
// Текст регистрации
Рассматривайте описатель hlnst как адрес исполняемого модуля в пространстве процесса, соответствующего приложению. Если вы не знакомы с понятиями поток и процесс, то обратитесь к последнему уроку этой книги, где приведены некоторые сведения об архитектуре Windows. Текст регистрации szWindowClass будет загружен из ресурсов при выполнении winMain (см. вызов LoadString).
При вызове функции winMain система передает ей параметры:
Ранее в Win 16 второй параметр использовался в целях экономии ресурсов, но в Win32 — это NULL, так как каждый экземпляр приложения теперь выполняется в своем собственном виртуальном адресном пространстве процесса емкостью 4 Гбайт. Все экземпляры процесса загружаются начиная с одного и того же адреса в этом пространстве (см. последний урок). Теперь рассмотрим алгоритм функции WinMain:
Основные атрибуты главного окна приложения задаются в структуре типа WNDCLASSEX. Понятие класс окна появилось до того, как появились классы C++. Поэтому структура WNDCLASSEX не имеет ничего общего с классами MFC. Она является типом структур языка С. Дело в том, что каждое Windows-приложение должно зарегистрировать атрибуты своего окна, а система использует их при создании окна. Структура WNDCLASSEX своими полями определяет некий шаблон или модель для создания окон данного класса. В полях структуры вы указываете необходимые атрибуты окна: адрес исполняемого модуля приложения, .адрес оконной процедуры, имя ресурса меню, набор битов для описания стиля окна, местонахождение изображения курсора, значка и т. д. Эту «неинтересную» работу выполняет большая часть кодов функции MyRegisterClass. Используя классы MFC, вы избавляете себя от подобной рутинной работы.
При регистрации класса окон (точнее, переменной типа WNDCLASSEX) операционная система связывает оконную процедуру (WndProc) с приложением. В winMain вы должны зарегистрировать главное окно приложения, остальные же окна, если они нужны, могут быть зарегистрированы и в других местах программы. Адрес заполненной структуры передается в функцию RegisterClassEx, которая говорит Windows, что от нее ожидается, когда окна подобного класса появляются на экране. Теперь система знает, какой вид курсора использовать при попадании указателя мыши в пределы окна, кому посылать сообщения о событиях, происходящих в области окна, какие значки (большой 32 х 32 и маленький 16 х 16) будут связаны с приложением и т. д. Функция RegisterClassEx возвращает число типа АТОМ (16-ти битное целое без знака), которое соответствует строке регистрации в таблице атомов, поддерживаемой системой.
После регистрации класса главного окна идет вызов функции Initlnstance, которая пытается создать окно (CreateWindow) зарегистрированного класса. Если система нашла класс окна в трех поддерживаемых ею списках зарегистрированных классов окон, то функция CreateWindow возвращает описатель окна (HWND). Мы запоминаем его для того, чтобы использовать в качестве параметра при вызове других функций. Если нет, то функция вернет нулевое значение и приложение молча завершает свою работу. Попробуйте при вызове CreateWindow вставить пустую строку, вместо szWindowClass. Запустив приложение, вы поймете, что в ветвь if (ihwnd) неплохо бы вставить вызов:
MessageBox(0,"Не нашла класс окна","Ошибка",МВ_ОК);
При успешном создании окна происходит вызов функций
//====== Показать окно
ShowWindow(hWnd, nCmdShow);
//====== Сделать это, минуя очередь
UpdateWindow(hWnd);
Далее в коде winMain загружается таблица акселераторов (соответствий клавиатурных комбинаций командам меню), которая используется в цикле ожидания и обработки сообщений. Функция GetMessage выбирает сообщение из очереди сообщений потока и помещает его в структуру типа MSG, служащей для хранения информации о сообщении Windows. Функция TranslateAccelerator пытается транслировать (преобразовать) сообщения WM_KEYDOWN (нажата клавиша) или WM_SYSKEYDOWN (нажата F10 или ALT+другая клавиша) в сообщения WM_COMMAND или WM_SYSCOMMAND, но только в том случае, если в таблице акселераторов присутствует код клавиши.
Преобразованное сообщение направляется непосредственно в оконную процедуру. Характерно то, что TranslateAccelerator ждет завершения обработки сообщения. Функция TranslateMessage транслирует сообщения, поступившие в виде виртуальных кодов клавиш, в символьные сообщения и снова отправляет их в очередь сообщений потока для того, чтобы отреагировать на него на следующей итерации цикла. Например, сообщение WM_KEYDOWN (virtual key message) она может преобразовать в WM_CHAR (character message) или в WM_DEADCHAR (см. MSDN). Функция DispatchMessage отправляет сообщение оконной процедуре.
Коротко алгоритм работы winMain может быть сформулирован так. После выполнения инициализирующих действий функция WinMain входит в цикл обработки сообщений. После выхода из этого цикла работа приложения завершается. Выход происходит, когда придет сообщение WM_QUIT. Обычно его посылает оконная процедура, когда пользователь закрывает главное окно.
Стартовая заготовка иллюстрирует стандартную последовательность действий при создании Windows-приложения на базе API-функций. Обратите внимание на то, что функция WndProc нигде явно не вызывается, хотя именно она выполняет всю полезную работу. Для проверки усвоения прочитанного ответьте на вопрос: «Когда и кем она вызывается?»
Геометрическое перо Косметические
UINT uHatch[] ={
HS_BDIAGONAL, HS_CROSS, HS_DIAGCROSS,
HS_FDIAGONAL, HS_HORIZONTAL, HS_VERTICAL
};
//===== Строки текста для пояснений
static string brush[] =
{
"HS_BDIAGONAL", "HS_CROSS", "HS_DIAGCROSS",
"HS_FDIAGONAL", "HS_HORIZONTAL", "HS_VERTICAL"
};
Вставьте следующий код в ветвь WM_PAINT перед вызовом EndPaint. Этот фрагмент по структуре такой же, как и предыдущий, но здесь мы создаем перо, используя штриховую (hatched) кисть. Запустите и проверьте, что получилось. Попробуйте объяснить, почему линия со штрихом типа HS_HORIZONTAL невидима. Замените строку
LineTo(hdc, iXMax, iYPos);
на
LineTo(hdc, iXMax, iYPos + 3);
и запустите вновь. Теперь линия должна быть видна. Найдите объяснение и попробуйте обойтись без последнего изменения кода, то есть уберите +3:
//======== геометричесое перо
Ib.lbStyle = BS_HATCHED; // Узорная кисть
sText = "Стили на основе кисти (Geometric pen)";
GetTextExtentPoint(hdc,sText.c_str(), sText.size(),SszText);
//======= Сдвиг позиции вывода
iYPos += 2 * szText.cy;
iXPos = iXCenter - szText.cx/2;
TextOut(hdc, iXPos, iYPos, sText.c_str() , sText.size ());
nLines = sizeof(brush)/sizeof(brush[0]);
for (i = 0; i < nLines; i ++ )
{
//======= Выбираем узор штриха кисти
Ib.lbHatch = uHatch[i];
//== Создаем на его основе перо тощиной 5 пиксел
HPEN hp = ExtCreatePen(PS_GEOMETRIC, 5, Sib,0,0);
HPEN hOld = (HPEN)SelectObject(hdc, hp) ;
iYPos += szText.cy; MoveToEx(hdc, 10, iYPos, NULL);
LineTo(hdc, iXMax,iYPos);
SelectObject(hdc, hold);
DeleteObject(hp);
TextOut(hdc, 10, iYPos, brush[i].c_str(), brush[i].size());
}
Геометрическое перо может быть создано на основе заданного пользователем или программистом узора (PS_USERSTYLE). Узор задается с помощью массива переменных типа DWORD. Элементы массива определяют циклический алгоритм закраски линии по схеме «играем — не играем». Например, массив DWORD а [ ] = { 2,3,4 } определяет линию, у которой 2 пиксела закрашены, 3 — не закрашены, 4 — закрашены. Затем цикл (2,3,4) повторяется. Для моделирования этой возможности введите в WndProc еще один двухмерный массив (так как линия будет не одна):
//======= три узора геометрического пера
static DWORD dwUser[3][4] =
{
{ 8, 3, 3, 3),
{ 3, 3, 3, 3),
(15, 4, 3, 4}
};
Затем добавьте вслед за фрагментом, моделирующим штриховую линию, следующий код:
//======= Геометричесое перо (user-defined)
Ib.lbStyle - BS_SOLID;
sText = "Стили на основе узора (Geometric pen)";
GetTextExtentPoint(hdc,sText.c_str(), sText.size(),SszText);
iYPos += 2 * szText.cy;
iXPos = iXCenter - szText.cx/2;
TextOutfhdc, iXPos, iYPos,
sText.c_str(), sText .size () } ,
nLines = sizeof(dwUser)/sizeof(dwUser[0]) ;
//====== Перебираем узоры пользователя
//====== (строки массива dwUser)
for (i = 0; i < nLines; i++)
{
DWORD dw = PS_GEOMETRIC | PS_USERSTYLE | PS_ENDCAP_FLAT;
HPEN hp = ExtCreatePen(dw, i+2, Sib, 4, dwUser[i]);
HPEN hOld = (HPEN)SelectObject(hdc, hp) ;
iYPos += szText.cy;
MoveToEx(hdc, 10, iYPos, NULL);
LineTo(hdc, iXMax,iYPos);
SelectObject(hdc, hold);
DeleteObject(hp);
TextOut(hdc, 10, iYPos, user[i].c_str(), user[i].size());
}
Запустите и проверьте результат. Здесь следует отметить, что узор имеет возможность развиться (разогнаться) только на достаточно большом промежутке между точками. Для вывода графиков функциональных зависимостей он, к сожалению, не пригоден, так как графики имеют большое количество точек. Точки расположены тесно, а узор начинается заново после каждой пары точек. Как ни странно, косметическое перо толщиной в 1 пиксел выдерживает подобное испытание и его можно использовать для графиков функций.
Косметическое перо Сначала исследуем
iXCenter; // центра окна,static int iXPos; // текущей позиции
static int iXMax; // допустимой позиции
int iYPos =0; // Текущая у-координата вывода
int nLines; // Количество линий
SIZE szText; // Экранные размеры строки текста
//====== Стили пера Windows
static DWORD dwPenStyle[] =
{
PS_NULL, PS_SOLID, PS_DOT, PS_DASH,
PS__DASHDOT, PS_DASHDOTDOT
};
//====== Строки текста для вывода в окно
static string style[] =
{
"PS_NULL","PS_SOLID","PS_DOT","PS_DASH",
"PS_DASHDOT","PS_DASHDOTDOT"
};
string sText; // Дежурная строка текста
//===== Логическая кисть — как основа для создания пера
LOGBRUSH lb = { BS_SOLID, color, 0 };
Если вы хотите, чтобы ваш вывод в окно реагировал на изменения пользователем размеров окна, то всегда вводите в оконную процедуру ветвь обработки WM_SIZE. Сделайте это сейчас вместе с изменениями в ветви WM_PAINT:
case WM_SIZE:
//==== В IParam упрятаны размеры окна.
//==== Нас интересует только ширина окна
iXMax = LOWORD(IParam) - 50;
iXCenter = LOWORD(IParam)/2; break;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
//===== Режим выравнивания текста (см. MSDN)
SetTextAlign(hdc, TA_NOUPDATECP | TA_LEFT | TA_BASELINE) ;
sText = "Стили линий в Win32 (Cosmetic pen)";
//== Выясняем размеры строки с текстом заголовка GetTextExtentPoint(hdc,sText.c_str(), sText.size(),
//== Сдвигаем точку вывода вниз на одну строку
iYPos += szText.cy;
iXPos = iXCenter - szText.cx/2;
//==== Выводим заголовок
TextOut(hdc,iXPos, iYPos, sText.c_str(), sText. size ()
}
//==== Перебираем массив стилей пера
nLines = sizeof(style)/sizeof(style[0]);
for (int i = 0; i < nLines; i++)
{
//===== Устанавливаем биты стиля пера
DWORD dw = PS_COSMETIC | dwPenStyle[i];
// Создаем перо толщиной в 1 пиксел
HPEN hp = ExtCreatePen(dw, 1, Sib, 0,NULL);
//===== Выбираем перо в контекст устройства
HPEN hOld = (HPEN)SelectObject(hdc,hp); iYPos += szText.cy;
// Сдвиг позиции
//===== Помещаем перо в точку (10, iYPos)
MoveToEx(hdc, 10, iYPos, NULL);
//==== Проводим линию до точки (iXMax, iYPos)
LineTo(hdc, iXMax, iYPos);
//== Возвращаем старое перо в контекст устройства
SelectObject(hdc, hold);
//=== Освобождаем ресурс пера DeleteObject(hp);
//=== Выводим поясняющий текст
TextOut(hdc, 10, iYPos, style[i].c_str(), style [i] .size ()
} ;
EndPaint(hWnd, &ps) ;
break;
Комментарии в тексте поясняют суть происходящего. Отметьте, что здесь применена стандартная тактика работы с ресурсами GDI, которая состоит из последовательности следующих шагов:
Так как система работает с ресурсами GDI динамически, то нарушение этой тактики может привести к недостатку памяти и непредсказуемому поведению приложения. Перед тем как запустить проект, попробуйте ответить на вопросы:
Теперь запустите приложение и протестируйте его, изменяя размеры окна и пользуясь диалогом. Как вы узнали из документации, косметическое перо может иметь толщину только в 1 пиксел. Если косметическое перо имеет еще один атрибут PS_ALTERNATE, то каждый второй пиксел линии пропускается (не выводится) и создается иллюзия, что перо стало тоньше, чем 1 пиксел. Опробуем эту возможность в нашем примере. Для этого введите в функцию WndProc еще один локальный массив подсказок.
static string alt[] = {"PS_ALTERNATE", "PS_COSMETIC" };
Вставьте следующий код в ветвь WM_PAINT перед вызовом EndPaint, затем запустите и проверьте результат:
//======= Косметическое перо (alternate - solid)
Ib.lbStyle = BS_SOLID;
sText = " Косметическое перо alternate или solid";
GetTextExtentPoint(hdc,sText.c_str(), sText.size(),SszText);
iYPos += 2 * szText.cy;
iXPos = iXCenter - szText.cx/2;
TextOut(hdc, iXPos, iYPos, sText.c_str(), sText.size());
for (i = 0; i < 2; i+ + ) {
DWORD dw = i ? PS_COSMETIC : PS_COSMETIC I PS_ALTERNATE;
HPEN hp = ExtCreatePen(dw, 1, &lb, 0, NULL);
HPEN hOld = (HPEN)SelectObject(hdc, hp) ;
iYPos += szText.cy;
MoveToEx(hdc, 10, iYPos, NULL);
LineTo(hdc, iXMax,iYPos);
SelectObject(hdc, hold);
DeleteObject(hp);
TextOut(hdc, 10, iYPos, alt[i].c str(), alt [i] . size ());
Оконная процедура Теперь рассмотрим
COLORREF color = RGB(255,0,0);//===== Массив цветов, выбираемых пользователем
static COLORREF CustColors[16];
Структура CHOOSECOLOR определена в библиотеке, которая сейчас недоступна, поэтому вставьте в конец файла stdafx.h директиву #include
#include
using namespace std;
так как ниже мы будем пользоваться объектами типа string из библиотеки STL. Затем в блок switch (wmld) функции WndProc введите ветвь обработки команды меню ID_EDIT_COLOR (саму команду создадим позже):
// Если выбрана команда с идентификатором ID_EDIT_COLOR
case ID_EDIT_COLOR:
// Подготовка структуры для обмена с диалогом
ZeroMemory(Sec, sizeof(CHOOSECOLOR));
//====== Ее размер
cc.lStructSize = sizeof(CHOOSECOLOR);
//====== Адрес массива с любимыми цветами
cc.lpCustColors = (LPDWORD)CustColors;
if (ChooseColor (ice)) // Вызов диалога
{
// Если нажата кнопка OK,
// то запоминаем выбранный цвет
color = cc.rgbResult;
// Объявляем недействительной
// клиентскую область окна
InvalidateRect(hWnd, NULL, TRUE);
}
break;
Функция ChooseColor запускает диалог в модальном режиме. Это означает, что пользователь не может управлять приложением, пока не завершит диалог. Тактика работы с диалогом такого типа стандартна:
Функция InvalidateRect сообщает системе, что часть окна стала недействительной, то есть требует перерисовки. В ответ на это система посылает приложению сообщение WM_PAINT. Наша оконная процедура уже реагирует на это сообщение, но пока еще не рисует. Теперь создадим команду меню, при выборе которой диалог должен появится на экране. Для этого:
Запустите приложение (Ctrl+F5) и опробуйте команду меню Edit > Color. Диалог имеет две страницы. Для того чтобы убедиться в правильном функционировании статического массива любимых цветов (custColors), раскройте вторую страницу, выберите несколько цветов в ее правой части, нажимая кнопку Add to Custom Colors. Обратите внимание на то, что выбранные цвета попадают в ячейки левой части диалога. Закройте и вновь откройте диалог. Новые цвета должны остаться на месте, так как они сохранились в массиве CustColors.
Перья на основе растровых изображений
UINT nPatterns[] ={
IDB_PAT1, IDB_PAT2, IDB_PAT3
};
static string bitmap!] =
{
"BS_PATTERN, 1", "BS_PATTERN, 2", "BS_PATTERN, 3"
);
Вслед за фрагментом, моделирующим стиль PS_USERSTYLE , вставьте следующий код:
//======= Геометричесое перо (bitmap)
Ib.lbStyle = BS_PATTERN;
sText = "Стили на основе bitmap-узора (Geometric pen)";
GetTextExtentPoint(hdc,sText.c_str(), sText.size 0,SszText);
iYPos += 2 * szText.cy;
iXPos = iXCenter - szText.cx/2;
TextOut(hdc, iXPos, iYPos, sText.c_str(), sText. size () ) ;
nLines = sizeof(nPatterns)/sizeof(nPatterns[0]);
for (i =0; i < nLines; i++)
{
HBITMAP hBmp;
hBmp = LoadBitmap(GetModuleHandle(NULL), (char*)nPatterns[i]);
Ib.lbHatch = long(hBmp);
HPEN hp = ExtCreatePen(PS_GEOMETRIC, 5, &lb, 0, 0) ;
HPEN hOld = (HPEN)SelectObject(hdc, hp) ;
iYPos += szText.cy;
MoveToEx(hdc, 10, iYPos, NULL);
LineTofhdc, iXMax,iYPos);
SelectObject(hdc, hOld);
DeleteObject(hp);
TextOut(hdc, 10, iYPos, bitmap[i].c_str(),
bitmap[i] .size () ) ;
}
Запустите на выполнение и проверьте результат. Вы должны получить окно, которое выглядит так, как показано на рис. 3.3. Отметьте, что остались неисследованными еще несколько возможностей по управлению пером — это стили типа PS_ENDCAP_* и PS_JOIN_*. Вы, вероятно, захотите их исследовать самостоятельно. При этом можете использовать уже надоевшую, но достаточно эффективную схему, которой мы пользуемся сейчас.

Рис. 3.3. Стили пера в Win32
Теперь ответим на один из вопросов, которые были заданы по ходу текста этой главы. Для того чтобы изменился цвет текста, надо вставить вызов API-функции SetTextColor. И сделать это надо в ветви обработки сообщения WM_PAINT перед вызовом функции TextOut.
SetTextColor(hdc, color) ;
Подведем итоги. Наш пример иллюстрирует характерные особенности строения приложения, управляемого событиями:
Программы, управляемые событиями
В этом уроке мы с помощью Studio.Net научимся разрабатывать традиционные приложения Win32, основанные на использовании функций API (Application Programming Interface). Вы, конечно, знаете, что приложения для Windows можно разрабатывать как с использованием библиотеки классов MFC, так и на основе набора инструментов, объединенных в разделе SDK (Software Development Kit) студии разработчика. Обширную документацию по SDK вы можете найти в MSDN (Microsoft Developer's Network), которая поставляется вместе со студией. Отдельные выпуски MSDN, зачастую содержат еще более свежую информацию по SDK. Без MSDN успешная деятельность в рамках студии разработчика просто немыслима.Использование всей мощи MFC облегчает процесс разработки приложений, однако далеко не все возможности Win32 API реализованы в библиотеке MFC, многие из них доступны только средствами API. Поэтому каждому разработчику необходимо иметь представление о структуре и принципах функционирования традиционного Windows-приложения, созданного на основе API-функций. Другими доводами в пользу того, что существует необходимость знать и постоянно углублять свои познания в технологии разработки приложений с помощью SDK, могут быть следующие:
В состав API входят не только функции, более 2000, но и множество структур, более 800 сообщений, макросы и интерфейсы. Цель настоящей главы:
Основной чертой всех Windows-приложений является то, что они поддерживают оконный интерфейс, используя при этом множество стандартных элементов управления (кнопки, переключатели, линейки, окна редактирования, списки и т. д.). Эти элементы поддерживаются с помощью динамических библиотек (DLL), которые являются частью операционной системы (ОС). Именно поэтому элементы доступны любым приложениям, и ваше первое приложение имеет почти такой же облик, как и любое другое. Принципиально важным отличием Windows-приложений от приложений DOS является то, что все они — программы, управляемые событиями (event-driven applications). Приложения DOS — программы с фиксированной последовательностью выполнения. Разработчик программы последовательность выполнения операторов, и система строго ее соблюдает. В случае программ, управляемых событиями, разработчик не может заранее предсказать последовательность вызовов функций и даже выполнения операторов своего приложения, так как эта последовательность определяется на этапе выполнения кода.
Программы, управляемые событиями, обладают большей гибкостью в смысле выбора пользователем порядка выполнения операций. Характерно то, что последовательность действий часто определяется операционной системой и зависит от потока сообщений о событиях в системе. Большую часть времени приложение, управляемое событиями, находится в состоянии ожидания событий, точнее сообщений о них. Сообщения могут поступать от различных источников, но все они попадают в одну очередь системных сообщений. Только некоторые из них система передаст в очередь сообщений вашего приложения. В случае многопотокового приложения сообщение приходит активному потоку (thread) приложения. Приложение постоянно выполняет цикл ожидания сообщений. Как только придет адресованное ему сообщение, управление будет передано его окопной процедуре.
Наступление события обозначается поступлением сообщения. Все сообщения Windows имеют стандартные имена, многие из которых начинаются с префикса WM_ (Windows Message). Например, WM_PAINT именует сообщение о том, что необходимо перерисовать содержимое окна того приложения, которое получило это сообщение. Идентификатор сообщения WM_PAINT — это символьная константа, обозначающая некое число. Другой пример: при создании окна система посылает сообщение WM_CREATE. Вы можете ввести в оконную процедуру реакцию на это сообщение для того, чтобы произвести какие-то однократные действия.
Программист может создать и определить какие-то свои собственные сообщения, действующие в пределах зарегистрированного оконного класса. В этом случае каждое новое сообщение должно иметь идентификатор, превышающий зарезервированное системой значение WM_USER (0x400). Допустим, вы хотите создать сообщение о том, что пользователь нажал определенную клавишу в тот момент, когда клавиатурный фокус находится в особом окне редактирования с уже зарегистрированным классом. В этом случае новое сообщение можно идентифицировать так:
#define WM_MYEDIT_PRESSED WM_USER + 1
Каждое новое сообщение должно увеличивать значение идентификатора по сравнению с WM_MYEDIT_PRESSED. Максимально-допустимым значением для идентификаторов такого типа является число 0x7 FFF. Если вы хотите создать сообщение, действующее в пределах всего приложения и не конфликтующее'с системными сообщениями, то вместо константы WM_USER следует использовать другую константу WM_APP (0x8000). В этом случае можно наращивать идентификатор вплоть до 0xBFFF.
Прохождение сообщений в системе
Путь прохождения сообщений от клавиатурыЗдесь буфер клавиатуры служит связующим звеном между прикладной программой и одним из сервисов ОС. Точно так же формируют (или могут формировать) свои специфические данные обработчики других событий. При этом используется универсальная структура данных MSG (сообщение), описывающая любое событие. Она содержит сопровождающую информацию, достаточную для того, чтобы сообщением можно было воспользоваться. Например, для сообщения от клавиатуры это должен быть код нажатой клавиши, для сообщения от мыши — координаты ее указателя, для сообщения WM_SIZE — размеры окна. Тип структур MSG определен в одном из файлов заголовков следующим образом:
//======= Ярлык типа
typedef struct tagMSG
{
//===== Описатель окна, чья оконная процедура
//===== получает сообщение
HWND hwnd;
UINT message; // Код сообщения
// Дополнительная информация, зависящая от сообщения
WPARAM wParam;
LPARAM iParam; // Тоже
DWORD time; // Время посылки сообщения
//==== Точка экрана, где был курсор
//==== в момент посылки сообщения
POINT pt;
}
MSG; //===== Тип структур, эквивалентный ярлыку
Универсальные параметры wParam и IParam используются различным образом в различных сообщениях. Например, в сообщении WM_LBUTTONDOWN первый из них содержит идентификатор одновременно нажатой клавиши (Ctrl, Shift и т.д.), а второй (IParam) — упакованные экранные координаты (х, у) курсора мыши. Чтобы выделить координаты, программист должен расщепить «длинный параметр» (4 байта) на два коротких (по 2 байта). В нижнем слове, которое можно выделить с помощью макроподстановки, например,
int х = LOWORD(IParam);
хранится координата х, а в верхнем — координата у, которую вы можете выделить с помощью макроса:
int у = HIWORD(IParam);
Отметьте, что классы библиотеки MFC избавляют вас от необходимости распаковывать параметры сообщения.
Следующая схема (рис. 3.2) в общих чертах иллюстрирует путь прохождения сообщений. Она любезно предоставлена Мариной Полубенцевой, вместе с которой мы ведем курс Visual C++ в Microsoft Certified Educational Center при Санкт-Петербургском государственном техническом университете.
Каждый обработчик события (драйвер) помещает сформированное сообщение в определенную динамическую структуру данных в памяти. Другие аппаратные и программные обработчики точно так же формируют свои сообщения, ставя их в очередь за уже существующими. Так формируется системная очередь сообщений.
Операционная система постоянно работает с очередью и, анализируя текущее сообщение, решает, какому приложению следует его передать. Она переписывает его в другую структуру данных в памяти — очередь сообщений конкретного приложения. Приложение реагирует или не реагирует на сообщение, но в любом случае удаляет его из очереди сообщений. Здесь важно понять, что по отношению к приложению сообщения появляются случайным образом и невозможно предсказать, какое сообщение появится в следующий момент.

Рис. 3.2. Путь прохождения сообщений Windows
Структура Windows-приложения Рассмотренная
msg.wParam;}
//
// FUNCTION: MyRegisterClass ()
//
// НАЗНАЧЕНИЕ: Регистрирует оконный класс
//
// COMMENTS: //
// Эта функция нужна только если вы хотите, чтобы код
// был совместим с Win32 системами, которые
// существовали до создания функции 'RegisterClassEx ' ,
// введенной в Windows 95.
// Вызов 'RegisterClassEx' необходим для правильного
// создания маленького (small) значка, ассоциированного
// с приложением.
//
ATOM MyRegisterClass (HINSTANCE hlnstance)
{
WNDCLASSEX wcex;
wcex.cbSize = sizeof (WNDCLASSEX) ;
wcex. style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = (WNDPROC) WndProc;
wcex. cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex. hlnstance = hlnstance;
wcex.hlcon = Loadlcon (hlnstance,
(LPCTSTR) IDI_API) ;
wcex.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wcex.hbrBackground = (HBRUSH) (COLOR_WINDOW+1) ;
wcex.lpszMenuName = (LPCSTR) IDC_API;
wcex. IpszClassName = szWindowClass;
wcex.hlconSm = Loadlcon (wcex. hlnstance, (LPCTSTR) IDI_SMALL)
return RegisterClassEx (&wcex) ;
}
//
// FUNCTION: Initlnstance (HANDLE, int)
//
// НАЗНАЧЕНИЕ: Запоминание описателя экземпляра
// приложения и создание главного окна приложения
//
// COMMENTS:
// В этой функции мы запоминаем описатель экземпляра
// приложения в глобальной переменной и создаем
// главное окно приложения.
//
BOOL Initlnstance(HINSTANCE hlnstance, int nCmdShow)
{
HWND hWnd;
//======= Запоминаем экземпляр приложения
hlnst = hlnstance;
//======= Создаем главное окно
hWnd = CreateWindow(szWindowClass, szTitle, WSJDVERLAPPEDWINDOW,CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hlnstance, NULL),
if (IhWnd) {
return FALSE; }
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd) ;
return TRUE; }
//
// FUNCTION: WndProc(HWND, unsigned, WORD, LONG)
//
// НАЗНАЧЕНИЕ: Обработка сообщений главного окна.
//
// WM_COMMAND - обработка команд меню
// WM_PAINT - перерисовка окна
// WM_DESTROY - посылка сообщения о завершении и выход
//
//
LRESULT CALLBACK WndProc (HWND hWnd, UINT message,
WPARAM wParam, LPARAM IParam)
{
int wmld, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
switch (message)
{
case WM_COMMAND:
wmld = LOWORD (wParam) ;
wmEvent - HIWORD (wParam) ;
//====== Расшифровка выбора в меню:
switch (wmld)
{
case IDM_ABOUT:
DialogBox (hlnst, (LPCTSTR) IDD_ABOUTBOX, hWnd,
(DLGPROC)About) ;
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, IParara);
{
break;
//======= Ветвь перерисовки содержимого окна
case WM_PAINT:
hdc = BeginPaint(hWnd, sps);
//======= TODO: Вставьте сюда рисующий код
EndPaint(hWnd, Sps);
break; //======= Ветвь закрытия окна
case WM_DESTROY:
PostQuitMessage(0);
break; default:
return DefWindowProc(hWnd, message, wParam, IParam);
}
return 0;
}
//======= Обработчик команды вызова диалога About
LRESULT CALLBACK About(HWND hDlg, UINT message,
WPARAM wParam, LPARAM IParam)
{
switch (message)
{
//======= Ветвь инициализации окна диалога
case WM_INITDIALOG:
return TRUE;
//======= Ветвь обработки команд, исходящих
//======= от элементов управления диалога
case WM_COMMAND:
if (LOWORD(wParam) == IDOK
LOWORD(wParam) == IDCANCEL)
EndDialog(hDlg, LOWORD(wParam));
return TRUE;
}
break;
}
return FALSE;
}
Традиционное Windows-приложение
Учебник по Visual C++ .Net
Анализатор кодов ошибок
Диалог About При нажатии кнопки
CLookDlg::OnSysCommand(UINT nID, LPARAM IParam){
if ((nID & OxFFFO) == IDM_ABOUTBOX)
CDialog(IDD_ABOUTBOX).DoModal();
else
CDialog::OnSysCommand(nID, IParam);
}
Здесь, как видно из кода, мы проверяем идентификатор команды, и если он соответствует команде About, то запускаем диалог в модальном режиме. Теперь необходимо вставить в меню управления окном команду About. Отметьте, что это меню создает каркас приложения и оно не имеет соответствующего ресурса в нашем приложении. Поэтому управление меню производится методами класса смени. Обычно это делают в функции OnlnitDialog. В этой же функции производят инициализацию элементов управления. Внесите в нее следующие изменения:
BOOL CLookDlg::OnlnitDialog()
{
//======= Добываем адрес меню управления окном
CMenu* pSysMenu = GetSystemMenu(FALSE) ;
if (pSysMenu)
{
//====== Добавляем команду About
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, "About...");
}
//====== Загружаем свой (нестандартный) значок
HICON hMylcon = ::Loadlcon(GetModuleHandle(0),(char*)(IDI_EYELEFT)); Setlcon(hMylcon, TRUE);
// Set big icon Setlcon(hMylcon, FALSE);
// Set small icon
//====== Если не удалось найти файл,
if (IReadErrors () }
{
PostMessage(WM_QUIT); // уходим
return FALSE;
}
//====== Количество элементов в контейнере
//=====преобразуем в строку
m_Total.Format("%d",m_nltems);
//====== Ищем и расшифровываем первый код ошибки
Getlnfo(0);
//====== Вызов родительской версии диалога
CDialog::OnlnitDialog ();
//====== Устанавливаем окно-двойник для счетчика
m_Spin.SetBuddy(GetDlgItem(IDC_CURRENT));
//====== Диапазон изменения показаний счетчика
m_Spin.SetRange(0, m_nlterns-1);
//===== Диапазон изменения позиции ползунка
m_Slider.SetRange(0, m_nlteras-l);
//===== Устанавливаем цену делений для шкалы ползунка m_Slider.SetTicFreq(m_nltems/10);
return TRUE;
}
Здесь показаны методы начальной установки показаний счетчика и позиции ползунка. Кроме того, мы сменили значок для окна приложения. Теперь это не IDI_WINLOGO, а наш глаз. Команда About добавляется в меню управления окном с помощью метода AppendMenu. Чтобы проверить правильность некоторых изменений, надо запустить приложение, но сначала надо ввести в состав ресурсов приложения идентификатор команды меню IDM_ABOUTBOX и временно исключить те фрагменты кода, которые еще не могут работать. Для задания нового идентификатора:
Полезным упражнением будет временное исключение (с помощью комментариев) того кода, который пока не может функционировать. Добейтесь того, чтобы код компилировался без ошибок. Затем можно запустить приложение и проверить работу меню и диалога About. Он должен вызываться как из меню, так и с помощью щелчка по картинкам. Скорее всего, он работать не будет. Я намеренно завел вас в эту ловушку, так как сам в нее попадался. Объяснение отказа можно найти в справке по функции OnSysCommand. Там сказано, что четыре младших бита параметра nio, который определяет идентификатор команды меню, используются Windows, поэтому в командах пользователя их следует обнулять. Мы это делаем путем побитового логического умножения на константу 0xFFF0:
if ((nID & 0xFFF0) == IDM_ABOUTBOX) CDialog(IDD_ABOUTBOX).DoModaK);
Но числовое значение идентификатора IDM_ABOUTBOX, которое было определено студией в диалоге Resource Symbols, скорее всего, не удовлетворяет этому условию и запуск диалога не производится. Чтобы изменить значение идентификатора, надо вновь открыть диалог Resource Symbols, найти IDM_ABOUTBOX в списке идентификаторов и изменить его значение, например на 112. Число должно быть больше того, которое предлагает студия, и делиться на 16, так как 4 младших бита должны быть нулями. После изменений такого рода полезно дать команду Build > Rebuild Solution и вновь запустить приложение. Если вы во всем разобрались, то диалог About должен работать.
Дизайн диалога Развитие диалогового
Окно диалога для поиска элементов управленияКоманда Rename > Tab из контекстного меню заголовка вставки позволяет переименовать всю вставку, а команда Rename > Item из контекстного меню самой вставки позволяет переименовать элемент. Команда Show > All > Tabs работает по принципу переключателя. Команда List > View, работающая по этому же принципу, позволяет переключать режим просмотра инструментов (значки/список). Команда Sort > Items > Alphabetically из контекстного меню заголовка вставки позволяет отсортировать инструменты, а команды Move > Up или Move > Down из меню окна — переместить их.
Поиск в реестре Алгоритм поиска
ok = ::RegOpenKeyEx (HKEY_LOCAL_MACHINE, vs, 0,KEY_READ, Shkey)== ERROR_SUCCESS && ::RegQueryValueEx (hkey,"ProductDir", 0, Sdw, (LPBYTE)path, &d) == ERROR_SUCCESS;
string sPath = "";
//====== Дополняем путь именем папки и файла
if (ok)
sPath = CString(path) + "\\Include\\Winerror.h";
return sPath; }
Поиск в реестре производится с помощью API-функций RegOpenKeyEx и RegQueryValueEx, первая из которых ищет и открывает ключ, заданный текстовой строкой, а вторая — ищет значение (value), связанное с открытым ключом. Результатом первой операции является Windows-описатель открытого ключа, который помещается по адресу hkey. Вторая операция требует задать hkey и имя искомого значения (в нашем случае — это ProductDir), а ее результатом является собственно значение (в нашем случае — полный путь к папке, где расположена Studio.Net). Если имя пусто, то функция возвращает значение по умолчанию (default).
Файл WinError.h расположен в папке Include, вложенной в папку Studio.Net. Поэтому мы дополняем найденный путь именем папки и файла. В случае неудачи при поиске папки наше приложение должно вести себя разумно, и поэтому мы предлагаем пользователю самому отыскать местоположение папки, где установлена студия. Это делается путем создания и вызова специального диалога по поиску папки. Диалог использует API-функции из группы Shell-API — подмножества API, которое поддерживается Shell32.dll и использует объекты COM (Component Object Model). Для успешной работы оболочки (shell) необходимо получить указатель на интерфейс (термин из COM) iMalloc, с помощью которого производится динамическое управление памятью. С технологией СОМ мы познакомимся позже, а сейчас введите в состав оконного класса еще одну функцию.
string CLookDlg::GetPathFromUser(void)
{
//====== Путь к файлу WinError.h пока пуст
string path = "";
//====== Указатель на интерфейс IMalloc
LPMALLOC pMalloc;
if (MessageBox("He могу найти папку,"
" где расположена Studio.Net"
" \r\rBu покажете путь к ней?",
"Поиск в реестре Windows",
MB_YES NO | MB_ICONQUESTION)==IDNO
FAILED( SHGetMalloc(&pMallo))
return path;
BROWSEINFO bi;
ZeroMemory (Sbi,sizeof(bi));
bi.ulFlags = BIF_RETURNONLYFSDIRS;
//====== Запуск диалога поиска папки
LPITEMIDLIST pidl = SHBrowseForFolder(&bi);
if (pidl) {
TCHAR szDir[MAX_PATH];
if (SHGetPathFromlDList(pidl,szDir))
{
path = szDir;
path += "\\Include\\Winerror.h"; }
pMalloc->Free(pidl); pMalloc->Release();
}
return path;
}
Попытка активизировать СОМ-объект и получить указатель на его интерфейс производится путем вызова функции SHGetMalloc. Первые две буквы SH означают принадлежность функции к семейству Shell-API. Макрос FAILED() проверяет на отрицательность возвращаемый функцией результат типа HRESULT, тем самым определяя факт неудачи. Структура BROWSEINFO помогает управлять параметрами диалога по поиску папки.
Функция SHBrowseForFolder запускает диалог, который позволяет пользователю выбрать папку. Она возвращает адрес списка идентификаторов pidl (pointer to identifier list), описывающих местоположение выбранной папки по отношению к корню (root) пространства имен (namespace). По умолчанию namespace — это рабочий стол (desktop). При работе с элементами СОМ важно помнить, что после использования интерфейса мы обязаны освободить его ресурсы вызовом метода Free и скорректировать (уменьшить на единицу) счетчик числа его пользователей (Release). Функция SHGetPathFromlDList преобразует список идентификаторов в системный файловый путь (szDir), который мы копируем в строку path.
Приложение на основе диалога Уверен
ERROR_ACCESS_DENIED 5LНадеюсь, вы не забыли, что суффикс ' L' задает тип константы (long). Вторая категория — это коды ошибок, возвращаемых многими OLE- и СОМ-АР1-функци-ями, например:
#define E_NOTIMPL _HRESULT_TYPEDEF_(Ox80004001L)
Последние имеют вид шестнадцатеричных чисел, которые хранятся в переменных типа HRESULT.
// Messageld: ERROR_FILE_NOT_FOUND
//
// MessageText:
//
// The system cannot find the file specified.
//
#define ERROR FILE NOT FOUND 2L
В файле есть несколько макроопределений, которые позволяют выделять некоторые параметры сообщения, но они не дают возможности программным способом выделить поле MessageText, так необходимое нам с вами. В файле приведены описания двух форматов кодов ошибок. Один из них определяет \¥ш32-ошибки, а другой — СОМ-ошибки. Оба имеют сходную структуру, но различаются в трактовке старших разрядов. Общее для них поле (Code) содержит относительный код или позицию ошибки в группе ошибок, связанных с той или иной ветвью в дереве Windows-технологий. Группы заданы кодом Facility. Например, группа, определяемая кодом Facility=3, объединяет ошибки работы с памятью, а группа Facility=17 объединяет все коды ошибок, которые могут возникнуть при использовании технологии СОМ+. Мощность множества вариаций атрибута Facility в версии Studio.Net 7.0 больше (23), чем в Visual Studio 6 (16), так как возросло количество поддерживаемых Windows технологий.
Таблица. 4.1 Формат кода 1Л/1п32-ошибок
31-30 | 29 | 28 | 27-16 | 15-0 | |||||||||
Severity | С | R | Facility | Code | |||||||||
Таблица. 4.2 Формат HRESULT СОМ-ошибок | |||||||||||||
31 | 30 | 29 | 28 | 27 | 15-0 | 26-16 | |||||||
S | R1 | С1 | N | r | Code | Facility | |||||||
Символы имеют следующий смысл:
Два старших бита Win32- oum6oK кодируют такие категории степени ошибки:
Зададимся целью разработать приложение, которое можно назвать анализатором кодов ошибок. С его помощью пользователь, зная код ошибки, сможет быстро получить всю информацию, которая хранится в файле WinError.h и связана именно с этой ошибкой. На примере разработки приложения мы продемонстрируем такие технологические приемы, как:
Основная идея приложения заключается в том, что при его открытии происходит попытка с помощью реестра найти файл WinError.h, сканировать его и заполнить динамический контейнер структур с информацией обо всех ошибках. Далее пользователь имеет возможность либо просматривать информацию об ошибках, последовательно проходя по элементам контейнера, либо ввести код ошибки и увидеть результат его трансляции (расшифровки). Форматы битовых полей HRESULT подсказывают состав полей структуры, которую можно использовать для хранения информации об ошибке:
//====== Тип стуктур для описания ошибок
struct ErrorType
{
string Code;
// Код ошибки string Identifier;
// Ее идентификатор string Message;
// Текстовое описание
//======= Конструктор с параметрами
ErrorType(string с, string i, string m)
{
Code = c;
Identifier = i;
Message = m;
}
};
Так как мы собираемся использовать контейнер структур такого типа, то полезно определить новый тип:
typedef vector
Определения такого типа упрощают создание ссылок на контейнеры или на его составные части. Перед тем как мы приступим к разработке приложения, отметим, что MFC-приложения на основе диалога имеют некоторые преимущества перед другими типами приложений. Главным из них является простота структуры классов и возможность пользоваться глобальными функциями MFC для обмена данными между окнами и переменными диалогового класса. Надо признать, что Эти функции (типа DDX_— Dynamic Data Exchange И DDV_ — Dynamic Data Validation) очень удобны и надежны. Конечно, приложения рассматриваемого типа не обладают такими разнообразными возможностями, как приложения типа MDI (Multiple Document Interface), но для определенного класса задач они являются оптимальным выбором.
Начнем с создания стартовой заготовки приложения, основанного на диалоге. Тип приложения, как вы помните, выбирается с помощью мастера MFC Application Wizard. В левой части своего окна он имеет список команд, которые играют роль вкладок, раскрывающих различные окна-страницы правой части окна, поэтому команды слева мы будем называть вкладками, а окна справа — страницами. Для создания заготовки:
Запустите стартовую заготовку и убедитесь, что она создает диалог со значком, двумя кнопками и текстом «TODO:..» Раскройте окно Class View и найдите на его панели инструментов кнопку с подсказкой Class View Sort By. Опробуйте все способы сортировки содержимого окна Class View. Наиболее удобным является режим Sort By Type, однако для начинающих будет полезен режим более подробной демонстрации классов и методов (Sort Group By Type). Выберите этот режим и раскройте узел с именем класса CLookDlg. Этот класс, происходящий от класса coialog, выполняет функции главного окна приложения. Теперь раскройте узел Functions и дважды щелкните на конструкторе класса. Просмотрите коды конструктора и других методов класса.
Вставьте в начало файла LookDlg.h (после директивы #pragma) определение типа структур ErrorType, которое было рассмотрено выше. Перед тем как мы начнем вносить другие изменения, упростим заготовку. Функция OnPaint, реагирующая на сообщение WM_PAINT, обычно не используется в диалоговых окнах, так как элементы управления, которыми начинен диалог, нет необходимости перерисовывать. Их перерисовывает каркас приложения без нашего участия. Каждый элемент управления имеет свою, зарегистрированную системой, оконную процедуру, которая и выполняет перерисовку. Однако в заготовке функция OnPaint присутствует, и она выполняет задачу, которая имеет малую важность, — перерисовывает значок на кнопке приложения (taskbar button) в его свернутом состоянии. Странным кажется тот факт, что пока мы-даже не можем свернуть окно. Вы заметили, что оно не имеет кнопки MinimizeBox, которая обычно располагается в правом верхнем углу окна. Запустите приложение и проверьте это. Сейчас мы исправим ситуацию, а заодно решим задачу со значком. Выполните следующие шаги для изменения класса CLookDlg:
Теперь в тело функции OnlnitDialog вместо двух строк:
SetIcon(m_hlcon, TRUE);
// Set big icon Setlcon(m_hlcon, FALSE);
// Set small icon
вставьте три строки, которые функционально заменяют весь тот код, который мы убрали. Функция Loadlcon загружает значок. Так как первый параметр функции задан равным нулю, то она не будет искать значок в ресурсах приложения, а возьмет стандартный (predefined) с идентификатором IDI_WINLOGO. Вы знаете, что символы «::», стоящие перед именем функции, означают, что эта функция является глобальной, то есть API-функцией. Эти символы можно и убрать, но тогда мы нарушим конвенцию (договоренность) об именах, существующую в сообществе программистов:
HICON hMylcon = ::Loadlcon(0,IDI_WINLOGO);
Setlcon(hMylcon, TRUE);
// Set big icon Setlcon(hMylcon, FALSE);
// Set small
Запустите приложение и убедитесь, что окно диалога теперь сворачивается и значок на месте.
Реакция окна на уведомляющие сообщения
tagNMHDR{
//=== Описатель окна (счетчика), пославшего сообщение
HWND hwndFrom;
//=== Идентификатор окна (счетчика)
UINT idFrora;
//=== Код сообщения
OINT code;
}
NMHDR;
Но на самом деле указатель pNMHDR содержит адрес другой структуры:
typedef struct _NM_UPDOWN
{
//====== Вложенная структура
NMHDR hdr;
//====== Текущая позиция счетчика
int iPos;
//====== Предлагаемое увеличение показаний
int iDelta;
}
NMUPDOWN, FAR *LPNMUPDOWN;
Так как структура hdr типа NMHDR стоит первой в списке полей NMUPDOWN, то все законно — присланный в iParam указатель действительно показывает на NMHDR, но в составе NMUPDOWN. Эту ситуацию легче запомнить, а может быть, и понять, если использовать аналогию. Способ запоминания замысловатых выкладок с помощью глупых аналогий известен давно. Мне приходит в голову такая: звонят в дверь (WM_NOTIFY), вы подходите к ней и видите, что пришел знакомый мальчик (NMHDR) с сообщением, но, открыв дверь, вы обнаруживаете, что за ним стоит широкоплечий мужчина (NMUPDOWN). Теперь пора ввести в класс CLookDlg реакции на уведомляющие сообщения:
Перейдите в окно LookDlg.cpp и найдите в карте сообщений новый элемент
ON_NOTIFY(UDN_DELTAPOS, IDC_SPIN, OnDeltaposSpin)
который был вставлен инструментом ClassWizard и который означает, что если окну диалога, управляемому классом CLookDlg, придет сообщение UDN_DELTAPOS (Up-Down Notification) от элемента с идентификатором IDC_SPIN, то управление будет передано функции-обработчику OnDeltaposSpin. Теперь в конце файла найдите эту функцию:
void CLookDlg::OnDeltaposSpin(NMHDR *pNMHDR, LRESOLT *pResult)
{
NM_UPDOWN* pNMUpDown = (NM_UPDOWN*)pNMHDR;
// TODO: Add your control notification handler code here
*pResult = 0; }
Вот здесь происходит то, о чем было сказано выше. Указатель PNMHDR приводится к типу указателя на более сложную структуру NM_UPDOWN. Это нужно для того, чтобы достать из нее необходимую информацию. Теперь с помощью указателя pNMUpDown мы можем добыть требуемое приращение показаний счетчика (pNMUpDown->iDelta). Вместо комментария // TODO: вставьте следующий фрагмент кода:
//====== Вычисляем желаемую позицию
int nPos = m_Spin.GetPos() + pNMUpDown->iDelta;
//====== Если она вне допустимых пределов, то уходим
if (nPos < 0 m_nltems <= nPos) return;
//====== Корректируем позицию ползунка
m_Slider.SetPos(nPos);
//====== Расшифровываем код ошибки
Getlnfo(nPos);
//====== Вызываем обмен данными с элементами окна диалога
UpdateData(FALSE);
Здесь уместно напомнить, что Studio.Net 7.0, как и Visual Studio 6, позволяет форматировать введенный текст так, как это принято в сообществе разработчиков. Выделите весь код функции и дайте команду Edit > Advanced > Format Selection или Alt+F8.
В коде мы используем данные (m_Spin, m_nltems, m_Slider) и метод (Getlnfо), которых еще нет в классе, но вы, наверное, имеете некоторый опыт программирования и знаете, что разработка часто идет в обратном порядке. Введем эти элементы в состав класса позже, а сейчас дадим оценку того, что только что сделали. С помощью ClassWizard мы ввели в класс главного окна обработку уведомляющего сообщения UDN_DELTAPOS, работающего по схеме WM_NOTIFY. Теперь введем обработку сообщения EN_CHANGE, поступающего от окна редактирования IDC_FIND каждый раз, когда в нем происходят изменения. Это сообщение работает по старой схеме и не влечет за собой необходимости преобразовывать указатели на структуры данных.
Проверьте результаты работы ClassWizard. Они должны быть видны в трех разных местах вашего приложения. В файле LookDlg.h должен появиться прототип функции обработки
void OnChangeFind (void) ;
в файле LookDlg.cpp должен появиться новый элемент карты сообщений
ON_EN_CHANGE(IDC_FIND, OnChangeFind)
и заготовка тела функции обработки, в которую мы должны внести свою функциональность:
void
CLookDlg::OnChangeFind(void)
{
// TODO: Если это RICHEDIT control, то он не пошлет
// уведомления пока вы не дадите своей версии функции
// CDialog::OnInitDialog() и не сделаете вызов функции
// CRichEditCtrl().SetEventMask() с флагом ENM_CHANGE,
// включенным с помощью операции побитового ИЛИ.
// TODO: Здесь вставьте код обработки уведомления.
}
В комментариях CLassWizard предупреждает нас о том, что с элементом типа Rich Edit control надо работать по особым правилам. К нам это не относится, поэтому уберите комментарии и вставьте вместо них такой код:
CString s;
//==== Выбираем код ошибки, введенный пользователем
GetDlgltemText(IDC_FIND, s) ;
//==== Преобразуем к типу string, с которым мы работаем
string find = s;
//==== Ищем код в контейнере
m_Vector
for (int n=0;
n < m_nltems is find != m_Vector[n].Code;n++);
if (n < m_nltems) // Если нашли,
{
Getlnfo(n); // то расшифровываем этот код
m_Slider.SetPos(n); // и синхронизируем ползунок
UpdateData(FALSE); // Высвечиваем данные в окнах
}
Переменная s типа CString понадобилась для того, чтобы воспользоваться функцией GetDlgltemText, которая вычитывает содержимое окна редактирования. Приходится делать преобразование к типу string, так как мы хотим работать со стандартными строками (string) библиотеки STL.
Возвращаясь к элементам управления в окне диалога, отметим, что ползунок тоже посылает уведомляющие сообщения по схеме WM_NOTIFY. Их всего три и вы можете их увидеть в окне Properties после нажатия кнопки ControlEvents, если предварительно установите фокус на элементе IDC_SLIDER. Одно из них — NM_RELEASEDCAPTURE — подходит нам, так как посылается в тот момент, когда пользователь отпускает движок после его установки мышью в новое положение. Но мы не будем вводить реакцию на это уведомление, так как есть другое (старое) сообщение Windows — WM_HSCROLL (или WM_VSCROLL при вертикальном расположении ползунка), которое работает более эффективно. Дело в том, что ползунок управляется не только мышью. Если он обладает фокусом, то реагирует на все клавиши перемещения курсора (4 стрелки, Page Up, Page Down, Home, End). Это очень удобно, так как позволяет гибко управлять темпом перемещения по многочисленным кодам ошибок. Введите реакцию оконного класса на сообщение WM_HSCROLL.
Отыщите изменения в классе CLookDlg. Их должно быть три. Отметим, что когда ClassWizard делает вставки в карту сообщений, то он пользуется своим опознавательным знаком — знаком комментария вида //}} AFX_MSG_MAP. Напомним, что в
Visual Studio 6 эти знаки существовали парами, а вставки между элементами пар отличались цветом. Теперь все упростилось. Введите код в тело функции-обработчика так, чтобы она была:
void CLookDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
//====== Расшифровываем новый код
Getlnfo(m_Slider.GetPos());
//====== Помещаем данные в поля диалога
UpdateData(FALSE); }
Сообщение WM_HSCROLL посылается в те моменты, когда ползунок изменяет свое положение как с помощью мыши, так и с помощью клавиш. В обработчике мы выявляем новую позицию ползунка, ищем и расшифровываем код, соответствующий этой позиции. Обратите внимание на то, что мы не пытаемся синхронизировать счетчик. Когда приложение будет работать, вы увидите, что последний, тем не менее, отслеживает изменения позиции ползунка. Попробуйте самостоятельно найти объяснение этому факту. Ответ можно найти в MSDN по теме CSpinButtonCtrl, если обратить внимание на то, что счетчик может иметь (Buddy) двойника-приятеля, в качестве которого мы уже выбрали окно редактирования IDC_CURRENT.
Синтаксический анализ файла До
GetNextErrorCode(ifstreams is){
//===== Поиск и выбор очередной ошибки из потока is
string s;
//==== Ищем строку текста "Messageld:"
int pos = FindText(is, s, "Messageld: ");
//==== Если дошли до конца файла, уходим if (is.eofO)
return false;
//=== Индекс ошибки следует за строкой "Messageld:
gsID = s.substr(pos);
//=== Ищем строку текста "MessageText: "
FindText(is, s, "MessageText:");
// Текстовое описание ошибки следует за пустым текстом
FindText(is, gsMsg="");
// Код ошибки (или HRESULT) следует за #define
FindText(is, s, "tdefine");
//== Ищем 'L' и стираем его и все, что за ним следует s.erase(pos=s.гfind("L"));
//======= Ищем пробел слева от кода
gCode = s.substr(s.rfind(" ",pos)+l);
//=== Ищем скобку, которая предшествует СОМ-ошибкам
if ( (pos=gCode.rfind("(")) != -1)
gCode.erase(0,pos+l); // Усекаем строку слева
return true;
}
Файл WinError.h не был введен вручную. Он сгенерирован специальной программой и поэтому имеет регулярную, стабильную структуру, специфику которой мы используем"для проведения синтаксического анализа и поиска нужной информации. Вы можете вновь обратиться к структуре текста, описывающего ошибку. Она приведена в начале главы. Использование заранее известных признаков, выделяющих искомый текст из всей массы символов, является характерным приемом при проведении синтаксического анализа текста. Так, мы знаем, что идентификатор ошибки следует за строкой «Messageld:» (с учетом пробела), поэтому мы ищем позицию конца этой строки и выбираем подстроку (substring) начиная с этой позиции:
gsID = s.substr(pos);
Алгоритм поиска строки текста реализован в функции FindText, которая еще не создана, но уже сейчас предъявлены к ней требования. Мы хотим, чтобы она построчно считывала файл и работала в двух режимах:
Пустой мы считаем строку, которая содержит только символы комментария и/ или символы пробела. Также известно, что текстовое описание ошибки следует за строкой «MessageText:». Поэтому после нахождения этой строки поиск запускается во втором режиме, когда FindText пропускает пробелы и символы комментария «//» и читает текст, следующий за ними. Здесь важно отметить, что некоторые сообщения занимают несколько строк комментария, следовательно, надо выбрать все строки и слить их в одну, разделив пробелом.
Далее мы выделяем код ошибки с учетом особенностей, связанных с тем, что существуют два различающихся между собой формата: Win32-onni6oK и СОМ-ошибок. Все коды СОМ-ошибок стоят в скобках, имеют суффикс' L ' и префикс _HRESULT_TYPEDEF_. Зная этот факт, мы проводим синтаксический анализ так, чтобы выделить числовой код. Осталось написать код функции FindText, требования к которой уже сформулированы. Она должна получать в параметрах:
Если последний параметр задать по умолчанию, то функцию можно использовать в двух режимах. Вставьте тело этой глобальной функции в начало файла LookDlg.cpp (после объявления массива gsFacilities):
int FindText (ifstreams is, strings s, TCHAR *text=NULL)
{
//=== Ищет подстроку
//=== Цикл прохода по всем строкам файла
for (int pos=-l; pos==-l && !is.eof(); )
{
//====== Считываем всю строку (до символа'\n')
getline (is, s, '\n');
//====== В первом режиме text не равен нулю
//====== и мы ищем этот текст, иначе ищем
//====== первый непустой символ
pos = text ? s.find(text) : s.find_first_not_of("/ ");
if (pos!=-l) // Если нашли
s.erase(0,pos); // Усекаем строку слева
} ;
// Если искали и нашли текст, то возвращаем его длину
// которая имеет смысл позиции, следующей за текстом
if (text && ! is.eof () )
return strlen (text) ;
// Если ищем непустую строку, то пропускаем все пустые
string st;
for (pos=0; pos!=-l && !is.eof(); )
{
getline(is,st,'\n');
pos = st.find_first_not_of("/ ");
//====== Если нашли непустой текст,
//====== то сливаем его с уже найденным
if (pos != -1)
s += ' ' + st.substr(pos);
}
// Возвращаем 0-ю позицию, так как нужен весь текст
return 0;
}
В такие моменты, когда программа почти готова, важно остановиться и вспомнить, не забыли ли мы выключить газ и свет. Я имею в виду освободить память, занимаемую какими-либо динамическими структурами. Мы используем контейнер структур, который необходимо где-то освобождать. Введите в public-секцию класса CLookDlg (в h-файле) деструктор этого класса:
~CLookDlgO { m_Vector.clear(); }
Это делать не обязательно, так как деструктор вызывается при выходе из приложения уже после того, как окно исчезло с экрана. Windows, закрывая процесс, освободит всю память, как в стеке, так и в heap, но правила хорошего тона говорят, что за собой надо убирать. Другим моментом, связанным с забывчивостью, является отсутствие директив #include для файлов заголовков используемых библиотек. Вы помните, что это следует делать в файле stdafx.h, чтобы пользоваться преимуществами pch-файла, который ускоряет повторные компиляции. Вставьте в конец файла stdafx.h следующие строки:
#include
//=== Буферизованные потоки, связанные с файлами (STL)
#include
#include
#include
//====== Работаем в пространстве имен std
using namespace std;
Запустите программу, устраните возможные синтаксические и семантические ошибки и хорошо протестируйте. Подведем итог:
Создаем диалог Важным моментом
Идентификаторы элементов управления диалогаТип элемента | Заголовок (комментарий) | Идентификатор | |||
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 |
Если не было ошибок ввода, то в теле функции DoDataExchange должно быть 6 строк вида DDX_Text. Процедура по созданию и связыванию переменных для окон редактирования почти не отличается от только что рассмотренной (для текстовых полей). Различия вы увидите в списке по выбору типа переменной (Variable Туре). Для элементов типа Edit Control существует множество преобразований вводимого текста в данные числовых типов (int, double и т. д.). С учетом сказанного создайте переменную cstring m_CurPos и свяжите ее с полем редактирования IDC_CURRENT.
Внесение логики разработчика Итак
CLookDlg: :ReadErrors (){
//==== Поиск и чтение информации об ошибках
//==== Пытаемся найти путь в реестре
string sPath = GetPathFromRegistry ( ) ;
//==== В случае неудачи пытаемся узнать у пользователя
if (sPath. empty () )
sPath = GetPathFromUser О ; if (sPath.emptyO)
return false; // При отказе уходим
//==== Пытаемся открыть файл
if stream is (sPath. c_str () ) ;
if (!is) {
MessageBox ("He могу найти WinError.h", "Выход") ;
return false;
//====== Последовательно ищем все ошибки
while (GetNextErrorCode (is) )
{
//==== Создаем новый объект типа ErrorType и
//==== помещаем его в контейнер
m_Vector.push_back (ErrorType (gCode, gsID, gsMsg) ) ;
}
is. closet);
// Закрываем файл
//====== Запоминаем размер контейнера
m_nltems = m_Vector . size () ;
return bool (m_nltems != 0) ;
}
Здесь мы вызываем функции (Getxxx), которых еще нет. Это является типичной практикой разработки многомодульных приложений. Мы определяем прототипы функций так, как того требует логика алгоритма, а тела будут созданы позже. Вы должны обратить внимание на объявление объекта is класса ifstream, который определен в STL. Поставьте курсор на имя класса ifstream и воспользуйтесь окном Dynamic Help, для того чтобы получить справку об этом классе. Из нее вы мало что узнаете, так как, к сожалению, все справки по библиотеке STL в разделе Reference слишком краткие, но если использовать поиск, то в MSDN можно получить достаточно подробную информацию о потоковом вводе-выводе.
В рассматриваемом коде мы вызываем конструктор класса ifstream, который создает поток ввода, связывает его с буфером и пытается открыть файл, путь к которому задан в параметре (sPath.c_str()). Вы помните, что вызов c_str() дает возможность пользоваться строкой в стиле языка с (то есть const char*), которая прячется в строке типа string. Операция "!", переопределенная в классе ifstream, сообщает нам о неудаче при открытии файла. Переменные gCode, gsio, gsMsg — это глобальные переменные, которые мы собираемся завести для временного хранения параметров ошибки (кода, индекса и сообщения).
В начало файла LookDlg.cpp (после директивы #endif) введите определения глобальных данных, которые будут использованы как в методах класса, так и в глобальных функциях:
//=== Текущие значения кода, индекса и текста сообщения
string gCode, gsID, gsMsg;
//====== Количество категорий (групп) ошибок
const int N_FACILITIES = 23;
//====== Имена категорий ошибок
TCHAR *gsFacilities[N_FACILITIES + 1] = {
"NULL", "RFC", "Dispatch",
"Storage", "Interface", "Unknown",
"Unknown", "Win32", "Windows",
"SSPI", "Control", "Cert",
"Internet", "MediaServer", "MSMQ",
"SetupAPI", "Smart Card", "COM+",
"AAF", "URT", "ACS",
"DPlay", "UMI", "SXS" };
Категории ошибок принято обозначать аббревиатурами, смысл которых можно выяснить в разделе Glossary MSDN. Например, аббревиатура RFC (Remote Procedure Call) обозначает категорию ошибок, связанных с обращением к процедурам, которые размещены на других процессорах сети.
Повторите последовательность действий по введению в класс вспомогательной функции и создайте функцию Getlnfo. Она выбирает из контейнера структуру, которая соответствует ошибке с индексом nPos, и присваивает переменным, связанным с элементами управления в окне диалога, значения, которые характеризуют ошибку (атрибуты ошибки). После такой операции можно проводить обмен данными (UpdateData(FALSE)) с дочерними окнами диалога и они «высветят» ошибку.
void CLookDlg::GetInfo(int nPos)
{
// ======= Текущая позиция
m_CurPos.Format("%d",nPos);
if (nPos >= m_nltems)
return;
//======= Выбираем поля структуры
m_Code = m_Vector[nPos].Code.c_str();
m_Msg = m_Vector[nPos].Message.c_str() ;
m_ID= m_Vector[nPos].Identifier.c_str();
//====== Преобразование кода в целое число
DWORD dw = strtoul(LPCTSTR(m_Code),0,0);
//====== Выделяем старший бит (Severity)
m_Severity = dw & 0x80000000 ? "Fail" : "Success";
//=== СОМ-коды это НЕХ-коды, длина которых > 8 символов
//=== В этой ветви мы обрабатываем Win32-ошибки
if (m_Code.GetLength() < 8)
{
if (dw)
{
//====== Вставляем поля facility и severity
dw = 0x80000000 | (0x7 << 16) | (dw f, OxFFFF) ;
m_Severity = "Error";
}
}
//====== Выделяем поле facility
UINT f = (dw»16) & 0xlFFF;
//====== Выбираем нужную аббревиатуру
m_Facility = f <= N_FACILITIES |gsFacilities[f) : "Unknown";
}
Так как коды \Ут32-ошибок не имеют полей facility и severity (эти атрибуты появились позже), то их надо синтезировать. Таким же образом поступает макроподстановка HRESULT_FROM_wiN32, и ее можно использовать в этом месте, но мы (с учебной целью) вставили ее код. Если вы хотите опробовать макрос, то замените строку
dw = 0x80000000 | (0x7 << 16) | (dw & 0xFFFF);
на
dw = HRESULT_FROM_WIN32(dw);
Далее мы выделяем поле facility и выбираем из массива gsFacilities аббревиатуру, которая более информативна, чем число f, кодирующее facility.
Вставка значка Если вы вновь посмотрите
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);
}
Учебник по Visual C++ .Net
Документ и его представления Библиотека
CTreeApp : public CWinApp{
public:
//====== Два шаблона документов
CMultiDocTemplate *m_pTemplDraw; CMultiDocTemplate *m_pTemplTree;
CTreeApp () ;
virtual BOOL Initlnstance();
afx_msg void OnAppAbout();
DECLARE_MESSAGE_MAP()
};
Класс CPolygon
В соответствии с архитектурой «документ — представление» мы должны ввести в класс документа некоторые новые структуры данных для хранения информации о файлах документов, обнаруженных в выбранной пайке или логическом диске. Файловые пути хранятся в контейнере текстовых строк типа vectortypedef vector
> VECPTS;
Эти типы данных мы разработали во втором уроке для обозначения множества реальных (World) координат точек изображаемых объектов. Перенесите указанные объявления из проекта My (см. урок 2) и вставьте их в начало файла TreeDoc.h до объявления класса CTreeDoc, но после директивы #pragma once. Вставляя объявление новых классов в тот же файл, мы экономим свои силы в процессе отладки приложения, потому что нам не надо так часто переключать окна и заботиться о видимости новых типов данных. Однако довольно часто при этом становятся невидимыми для новых классов старые типы, которые декларированы в этом же файле, но чуть ниже. Такие проблемы легко решаются с помощью упреждающих объявлений класса. Вставьте сразу за директивой #pragma once такое объявление:
class CTreeDoc; // Упреждающее объявление
В конец файла StdAfx.h вставьте строки, которые обеспечивают видимость некоторых ресурсов библиотеки STL:
#include
Кроме того, нам понадобится новый полноценный класс, который инкапсулирует функциональность изображаемого объекта. Объекты этого класса должны быть устойчивы, то есть должны уметь сохранять и восстанавливать свое состояние, также они должны уметь правильно изображать себя в любом контексте устройства, который будет подан им в качестве параметра. Все перечисленные свойства «почти бесплатно» получают классы, произведенные от класса библиотеки MFC cobject. Вставьте в файл TreeDoc.h после строки с определением типа VECPTS, но до объявления класса CTreeDoc, объявление класса CPolygon:
class CPolygon: public CObject
{
DECLARE_SERIAL(CPolygon)
public:
CTreeDoc *m_pDoc; // Обратный указатель
VECPTS m_Points; // Контейнер вещественных точек
UINT m_nPenWidth; // Толщина пера
COLORREF m PenColor; // Цвет пера
COLORREF m_BrushColor; // Цвет кисти
CDPoint m_ptLT; // Координата левого верхнего угла
CDPoint m_ptRB; // Координата правого нижнего угла
//====== Конструктор по умолчанию
CPolygon () ;
//====== Конструктор копирования
CPolygon(const CPolygons poly);
//====== Операция присвоения
CPolygons operator= (const CPolygons poly);
//====== Операция выбора i-той точки
CDPointS operator!] (UINT i);
//====== Вычисление обрамляющего прямоугольника
void GetRect(CDPointS ptLT, CDPointS ptRB);
//====== Установка обратного указателя
void Set (CTreeDoc *p); //====== Изменение атрибутов
void SettCTreeDoc *p,COLORREF bCl,COLORREF pCl,UINT pen);
//====== Создание трех простых заготовок
void MakeStar();
// Звезда
void MakeTria();
// Треугольник
void MakePent(); // Пятиугольник
//====== Изображение в контексте устройства
virtual void Draw (CDC *pDC, bool bContour);
//====== Сохранение и восстановление данных
virtual void Serialize(CArchiveS ar);
virtual ~CPolygon(); // Деструктор
//====== Новый тип данных: контейнер полигонов
typedef vector
Каждый объект класса CPolygon должен иметь связь с данными документа. Это осуществляется путем запоминания адреса документа в переменной m_pDoc, которая играет роль обратного указателя. Такой прием, когда вложенный объект помнит адрес объемлющей его структуры данных, очень распространен в объектно-ориентированном программировании. Он существенно упрощает обмен данными между двумя объектами.
Обилие методов класса CPolygon сделано «на вырост». Сейчас каждый документ для простоты представлен одним полигоном. Реальные конструкции можно задать в виде множества полигонов. При этом каждый из них должен знать свои габариты. Метод GetRect позволяет вычислять и корректировать габариты полигона. Если вы будете применять эти идеи в более сложном проекте, то вам понадобится множество других методов. Например, методы, определяющие факт самопересечения полигона или взаимного их пересечения.
Главными методами, которые реализуют концепцию архитектуры «документ — представление», являются Serialize и Draw. Метод Serialize позволяет общаться с файлами. Его особенность состоит в том, что он позволяет как записывать все данные объекта в файл, точнее в архив, так и читать их из файла. Здесь опять проявятся преимущества наследования от cobject, так как объекты классов, имеющих такого авторитетного родителя, обычно сами умеют себя сериализовывать.
Новый класс CPolygon должен иметь родителя CObject, с тем чтобы он мог воспользоваться его мощным оружием — сериализацией. При этом в объявлении класса должен присутствовать макрос:
DECLARE_SERIAL(CPolygon)
который влечет свое продолжение — другой макрос
IMPLEMENT_SERIAL(CPolygon, CObject, 1)
Последний должен быть расположен в файле реализации класса. Третий параметр (wSchema) этой макроподстановки задает номер версии приложения. Номер схемы кодируется и помещается в архив вместе с другими сохраняемыми данными. Это позволяет корректно обойтись в такой ситуации.
Предположим, что имеются файлы с расширением mgn, в которых хранятся данные о магнитах, созданных нашим приложением. Затем допустим, что мы внесли изменения в коды приложения и добавили в класс CPolygon еще одно какое-то поле данных. Теперь, записывая данные в архив (файл), также получим файл с расширением mgn, но другого формата. После этого мы не сможем правильно читать старые файлы. Если не предпринять никаких мер, то данные будут прочитаны неверно, а это часто приводит к непредсказуемому поведению программы. Механизм версий справляется с этой проблемой, но вы не должны забывать вовремя менять номер версии. При каждом изменении в структуре сохраняемых данных следует изменять номер версии. При попытке прочитать файл, соответствующий другой версии, каркас приложения просто выдаст сообщение о несовпадении версий и закроет файл данных.
С учетом сказанного рассмотрим, как должна выглядеть реализация нового класса. Следующие функции и макросы необходимо поместить в начало файла TreeDoc.cpp, после директив препроцессора:
IMPLEMENT_SERIAL(CPolygon, CObject, 1)
//====== Конструктор по умолчанию
CPolygon::CPolygon()
{
m_pDoc = 0; // Пока не знаем обратного адреса
MakeStarO; // Зададим полигон в виде звезды
}
Класс для нового представления
"DrawView.h"Внесите изменения в интерфейс класса, так чтобы он стал:
#pragma once
class CTreeDoc; // Упреждающее объявление
class CDrawView : public CView {
DECLARE_DYNCREATE(CDrawView) protected:
CSize m_szView; // Размеры клиетской области окна
bool m_bNewPoints; // Флаг режима вставки новых точек
bool m_bReady; // Флаг готовности захвата вершины
bool m_bLock; // Флаг захвата вершины
int m_CurID; // Индекс полигона в массиве
HCURSOR m_hGrab; // Курсор захвата
CPen m_penLine; // Перо для изображения контура
CDrawView();
virtual ~CDrawView();
public:
CTreeDoc* GetDocument()
{
return dynamic_cast
}
virtual void OnDraw(CDC* pDC);
//====== Настройка контекста устройства
void SetDC(CDC* pDC);
//====== Перерисовка контура
void RedrawLines (CDC *pDC, CPointS point);
DECLARE_MESSAGE_MAP()
};
Так как мы ввели в класс новый метод GetDocument и тут же дали коды его реализации, то класс CTreeDoc должен быть известен компилятору до того, как он познакомится с классом CDrawView. Вставьте строку с директивой включения файла заголовков
#include "TreeDoc.h"
в список директив файла DrawView.cpp до строки, подключающей файл DrawView.h. Класс нового представления старого документа имеет простое назначение: изобразить в центре своего окна дежурный полигон m_Poly, имеющийся в составе документа. Для упрощения этой задачи мы ввели в класс переменную CSize m_szView, которая будет хранить текущие размеры клиентской области окна. Несколько позже мы дадим коды методов визуального редактирования. Эти методы используют параметры текущего состояния, которые надо инициализировать в конструкторе класса. Откройте файл с кодами реализации класса (DrawView.cpp) и измените конструктор и функцию перерисовки OnDraw:
CDrawView::CDrawView()
{
//====== Всё режимы редактирования выключены
m_bNewPoints = false;
m_bReady = false;
m_bLock = false;
m_CurID = -1;
}
void CDrawView: :OnDraw(CDC* pDC) { CTreeDoc* pDoc = GetDocument ();
{
//====== Настройка контекста устройства
SetDC(pDC) ;
//====== Если вершина перемещается,
//====== рисуем без заливки внутренних областей,
pDoc->m_Poly .Draw(pDC, m_bLock) ;
}
В режиме редактирования полигон рисуется без заливки внутренних областей, а в обычном режиме просмотра — с заливкой. Режим выбирает пользователь, а переменная m_bLock следит за тем, какой режим выбран. Настройка контекста устройства определяет трансформацию изображения: увеличение и сдвиг, по формуле, обсуждавшейся в уроке 2. Метод Setoc позволяет учесть текущие размеры окна:
void CDrawView: :SetDC(CDC* pDC)
{
CTreeDoc* pDoc = GetDocument ();
//====== Режим преобразования без искажений пропорций
pDC->SetMapMode (MM_ISOTROPIC) ;
//======Размеры логического окна хранит документ
pDC->SetWindowExt (pDoc->m_szDoc) ;
pDC->SetWindowOrg (pDoc->m_szDoc.cx/2, pDoc->m_szDoc.cy/2) ;
//====== Размеры физического окна хранит представление
pDC->SetViewportExt (m_szView.cx, -m_szView. су) ;
pDC->SetViewportOrg (m_szView.cx/2,. m_szView.cy/2) ;
}
Способом, который вы уже не раз применяли, введите в класс CDrawView реакцию на сообщение WM_SIZE и измените тело функции-обработчика.
void CDrawView: :OnSize(UINT nType, int ex, int су)
{
CView: :OnSize (nType, ex, cy) ;
// Каркас иногда вызывает эту функцию с нулевыми сх,су
if (cx==0 | | су==0)
return;
//====== Запоминаем размеры окна
m_szView = CSize (ex, cy) ;
}
Вспомните способ замещения виртуальных функций (Overrides) и используйте его для введения в класс заготовки функции OnlnitialUpdate. Введите в нее код для подготовки инструментов, которые понадобятся в процессе визуального редактирования данных:
void CDrawView::OnInitialUpdate() {
//====== Загружаем курсор перемещения
m_hGrab=((CTreeApp*)AfxGetApp())->LoadCursor(IDC_MOVE);
//=== Создаем перо перерисовки контура (при перемещении)
m_penLine.CreatePen (PS_DOT,О,COLORREF(0)); }
Настала очередь создания второго участника квартета, определяющего поведение окна MDI-документа. Это заявленный нами класс CDrawFrame. Для его создания повторите те же действия, которые вы производили при создании класса CDr awView, но при выборе родительского класса укажите на класс cMDichildWnd (без параметра splitter). Представьте приложению нового оркестранта, вставив директиву
#include "DrawFrame.h"
в список уже существующих директив файла Тгее.срр. Запустите приложение. Если вы не допустили ошибок или устранили их, то должны увидеть диалоговое окно New со списком из двух строк: Tree и Draw. Выбрав Draw, вы должны увидеть окно документа с заголовком Drawl и изображенной в центре окна звездой. Нажмите кнопку New на панели инструментов и во вновь появившемся диалоговом окне выберите на сей раз шаблон Tree. В меню Window выберите Tile, и вы увидите два окна, причем второе будет иметь заголовок Treel. Переводя фокус из одного окна в другое, обратите внимание на смену строк меню главного окна. Значки в верхнем левом углу окон документов тоже должны быть разными. Панели инструментов, как мы уже отмечали, автоматически не изменяются. Эту функциональность мы внесем позже.
Класс для просмотра изображений
//====== Класс для демонстрации содержимого документовclass CRightView : public CScrollView {
//====== Упреждающее объявление класса картинок
friend class CWndGeom; protected:
CSize m_szView; // Реальные размеры окна
CSize m_szScroll; // Размеры прокручиваемого окна
CSize m_szltem; // Размеры картинки
CSize m_szMargin; // Размеры полей
CString m_WndClass; // Строка регистрации картинки
CRightView () ;
DECLARE_DYNCREATE(CRightView) public: //====== Контейнер картинок
vector
CTreeDoc* GetDocument()
{
return dynamic_cast
}
virtual -CRightView();
void Show(); // Демонстрация картинок
void Clear();
// Освобождение ресурсов
// Overrides public:
virtual void OnDraw(CDC* pDC) ;
protected:
virtual void OnlnitialUpdate() ;
DECLARE_MESSAGE_MAP() };
Внесите сокращения и изменения в коды реализации класса так, как показано ниже:
IMPLEMENTJDYNCREATE(CRightView, CScrollView)
BEGIN_MESSAGE_MAP(CRightView, CScrollView) END_MESSAGE_MAP()
CRightView::CRightView()() CRightView::-CRightView(){}
void CRightView::OnDraw(CDC* pDC)
{
CTreeDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc);
}
Полосы прокрутки автоматически появляются, когда реальные размеры окна (m_szview) становятся меньше размеров прокручиваемого окна (m_szScroll), которые надо задать в качестве аргумента функции SetScrollSizes. Если пользователь увеличил размеры окна и они стали равными или больше тех, что были указаны, то полосы автоматически исчезают. Отсюда следует, что программист должен как-то задать первоначальные размеры m_szScroll, когда еще не известны требования к ним. Обычно это делается в функции OnlnitialUpdate. Просмотрите коды этой функции, и вы увидите, какие размеры прокручиваемого окна (по умолчанию) задал мастер AppWizard. Для слежения за размерами окна представления введите в класс CRightview реакцию на сообщение WM_SI ZE, так же как вы это делали в классе CDrawView. Измените коды этой функции, а также функции OnlnitialUpdate, в которой мы приравниваем начальные размеры прокручиваемого окна к реальным:
void CRightView::OnSize(UINT nType, int ex, int cy)
{ CScrollView::OnSize(nType, ex, cy) ;
if (cx==0 cy==0)
return;
//====== Запоминаем размеры окна представления
m_szView = CSize (ex, cy);
}
void CRightView::OnInitialUpdate()
{
CScrollView::OnInitialUpdate();
//====== Начальные размеры окна
m_szScroll = m_szView; SetScrollSizes(MM_TEXT, m_szScroll) ;
}
Функция SetScrollSizes одновременно с размерами задает и режим преобразования координат. Самым неприятным и непонятным моментом в наследовании от класса CScrollView является то, что функция SetScrollSizes не позволяет задавать режимы MM_ISOTROPIC и MM_ANISOTROPIC, которые позволяют, как вы помните работать с формулами. Этот недостаток MFC широко дискутировался как в MSDN, так и на одном из самых популярных сайтов для программистов — www. CodeGuru.com. Там же вы можете обнаружить некоторые решения этой проблемы. Измените конструктор класса. В момент своего рождения объект класса CRi'ghtView должен подготовиться к работе с окнами, управляемыми классом CWndGeom. К тому моменту, когда ему понадобится создать серию таких окон, их тип (класс окон в смысле структуры типа WNDCLASS) уже должен быть известен системе.
CRightView::CRightView() {
m_szltem = CSize (200,150); // Размеры картинки
m_szMargin = CSize (20,20); // Размеры полей
try
{
//====== Попытка зарегистрировать класс окон
m_WndClass=AfxRegisterWndClass(CS_VREDRAWICS_HREDRAW, ::LoadCursor(GetModuleHandle(0),(char*)IDC_MYHAND), (HBRUSH)CreateSolidBrush(GetSysColor(COLOR_INFOBK)));
}
catch (CResourceException* pEx)
{
AfxMessageBox(_T("Класс уже зарегистрирован")); pEx->Delete ();
}
}
В конструкторе класса CRightView происходит попытка зарегистрировать новый класс окон. Обычно отказов здесь не бывает, но технология требует проверить наличие сбоя, поэтому включаем механизм обработки исключений (try-catch). Мы хотим добиться особого поведения окон с картинками, поэтому зададим для них свою форму курсора и свой цвет фона. Цвет фона выбирается из того набора, который предоставляет система (см. справку по функции GetSysColor), а курсор создали сами. Дело в том, что системный курсор, идентифицируемый как i DC_HAND, работает не во всех версиях Windows. Если вы работаете в среде Windows 2000, то можете заменить в параметре функции LoadCur sor вызов GetModuleHandle (0) на 0, а идентификатор IDC_MYHAND на IDC_HAND и работать с системным курсором. В этом случае ресурс курсора IDC_MYHAND окажется лишним и его можно удалить.
В данный момент мы предполагаем, что в классе документа уже создан динамический контейнер m_Shapes объектов класса CPolygon, каждый элемент которого соответствует данным, полученным в результате чтения документов, обнаруженных в текущем каталоге. Теперь приступим к разработке самой сложной функции в составе класса CRightView, которая должна:
Дальше события развиваются автоматически. После создания окна cwndGeom система пошлет ему сообщение WM_PAINT, в обработке которого надо создать и настроить контекст устройства мини-окна, а затем вызвать функцию Draw для того полигона из контейнера m_Shapes, индекс которого соответствует индексу окна CWndGeom. Каждый полигон рисует себя сам в заданном ему в качестве параметра контексте устройства. Введите в файл реализации класса CRightView следующий код:
void CRightView::Show()
{
CTreeDoc *pDoc = GetDocument0;
//====== Количество картинок
int nPoly = pDoc->m_Shapes.size();
//=== Вычисление шага, с которым выводятся картинки
int dx = m_szltem.cx + m_szMargin.ex,
dy = m_szltem.cy + m_szMargin.cy,
nCols = m_szView.cx/dx; // Количество колонок
//====== Коррекция
if (nCols < 1)nCols = 1;
if (nCols > nPoly)nCols = nPoly;
//====== Количество рядов
int nRows = ceil(double(nPoly)/nCols);
//=== Вычисление и установка размеров окна прокрутки
m_szScroll = CSize(nCols*dx, nRows*dy);
SetScrollSizes(MM_TEXT, m_szScroll);
//====== Координаты и размеры первой картинки
CRect r (CPoint(0,0), m_szltem);
r.OffsetRect (15,15);
//====== Стиль окна картинки
DWORD style = WS_CHILD | WS_BORDER | WS_VISIBLE;
//====== Цикл прохода по рядам (n - счетчик картинок)
for (int 1=0, n=0; i
{
//====== Цикл прохода по столбцам
for (int j=0; j
{
//====== Создаем класс окна картинки
CWndGeora *pWnd = new CWndGeom(this, n);
//====== Запоминаем его в контейнере
m_pWnds.push_back(pWnd);
//====== Создаем Windows-окно
pWnd->Create (m_WndClass, 0, style, r, this, 0);
//====== Сдвигаем позицию окна вправо
r.OffsetRect (dx, 0);
}
//=== Начинаем новый ряд картинок (сдвиг влево-вниз)
r.OffsetRect (-nCols*dx, dy);
}
}
Существенным моментом в алгоритме является то, что размер прокручиваемого окна (m_szScroll) зависит от количества картинок. Поэтому сколько бы их не было в текущей папке — все будут доступны с помощью полос прокрутки. Расположение и размеры картинок определяются с помощью объекта класса CRect. Метод Of f setRect этого класса позволяет сдвигать прямоугольник окна в нужном нам направлении.
Обслуживание контейнера m_pWnds дочерних окон типа cwndGeom сопряжено с необходимостью следить за освобождением памяти, занимаемой окнами, в те моменты, когда происходит переход от папки к папке в окне CLef tview. Для этой цели служит вспомогательная функция Clear, которую надо вызывать как в отмеченные выше моменты, так и при закрытии окна. Последний случай сопровождается автоматическим вызовом деструктора класса CRightview. С учетом сказанного введите такие добавки в файл RightView.cpp:
void CRightview::Clear()
{
//====== Цикл прохода по всем адресам контейнера
for (UINT i=0; Km_pWnds. size () ; i++)
{
//====== Уничтожение Windows-окна
m_pWnds[i]->DestroyWindow();
// Освобождение памяти, занимаемой объектом
delete m_pWnds[ i ] ;
}
//===== Освобождение памяти, занимаемой контейнером m_pWnds.clear();
}
//===== Деструктор класса вызывает
Clear CRightview::~CRightview()
{
Clear () ;
}
Конструкторы и операции Важными
CPolygonS poly){//====== Копируем все данные
m_pDoc = poly.m_pDoc;
m_nPenWidth = poly.m_nPenWidth;
m_PenColor = poly.m_PenColor;
m_BrushColor = poly.m_BrushColor;
m_ptLT = poly.m_ptLT;
m_ptRB = poly.m_ptRB;
//===== Освобождаем контейнер точек
if (!m_Points.empty()) m_Points.clear();
//====== Копируем все точки. Возможно решение с помощью assign.
for (OINT i=0; i
//====== Возвращаем собственный объект
return *this;
//====== Конструктор копирования пользуется уже
//====== существующей реализацией операции присвоения
CPolygon::CPolygon(const CPolygoni poly)
{
*this = poly;
}
Довольно часто во вновь создаваемых классах переопределяют операцию выбора с помощью угловых скобок ( [ ] ). Смысл этой операции задает программист. Он часто бывает очевидным для классов объектов, содержащих внутри себя контейнеры, как в нашем случае. Так, если к полигону poly применить операцию выбора, например
CDPoint pt = poly[i];
то он возвратит свою i-ю точку, что, безусловно, имеет смысл. Если же операция [ ] возвращает ссылку на i-ю точку, то становится возможным использовать ее и в левой части операции = (присвоения). Например,
poly[i] = CDPoint (2.5, -20.);
Отметим, что в новом языке С#, который поддерживается Studio.Net 7.0, такой прием является встроенным средством языка под названием indexer. С учетом сказанного введите следующую реализацию операции [ ]:
CDPointS CPolygon::operator[](UINT i)
{
if (0 <= i && i < m_Points.size ())
return m_Points[i];
return m_ptLT;
}
Функция Set для установки обратного указателя может быть совмещена (overloaded) с одноименной функцией, позволяющей изменять атрибуты изображения полигона:
//====== Установка обратного указателя
void CPolygon::Set (CTreeDoc *p) { m_pDoc = p;
{
//====== Совмещенная версия для изменения атрибутов
void CPolygon::Set (CTreeDoc *p, COLORREF bCl, COLORREF pCl, UINT pen)
{
m_pDoc = p;
m_BrushColor= bCl;
m_PenColor = pCl;
m_nPenWidth = pen;
}
Деструктор класса должен освобождать память, занимаемую вложенным в объект контейнером точек:
CPolygon::~CPolygon()
{
m_Points.clear() ;
}
Метод GetRect получает на входе ссылки на две характерные точки прямоугольника, обрамляющего весь полигон, вычисляет координаты этих точек и возвращает их с помощью механизма передачи ссылкой:
void CPolygon::GetRect(CDPointS ptLT, CDPointi ptRB)
{
m_ptLT = m_ptRB = CDPoint(0., 0 .) ;
//====== Если полигон содержит точки контура
UINT n = ra_Points.size();
if (n > 0)
{
//====== Пробег по всем его точкам
for (UINT 1=0; i
{
//====== Поиск и запоминание экстремумов
double х = m_Points[i].x,
у = m_Points[i].у;
if (x < m_ptLT.x) m_ptLT.x = x;
else if (x > m_ptRB.x)
m_ptRB.x = m_Points[i].x; if (y > m_ptLT.y) ra_ptLT.y = y;
else if (y < m_ptRB.y)
m_ptRB.y = y;
}
}
//====== Возвращаем найденные координаты (ссылками)
ptLT = m_ptLT; ptRB = m_ptRB;
}
Метод сериализации данных полигона, приведенный ниже, мог бы быть более компактным, если бы для хранения точек полигона мы воспользовались бы одним из шаблонов семейства классов Collection библиотеки MFC. В эти классы уже встроена возможность сериализации. Но у нас на вооружении шаблон классов vector из другой библиотеки STL, так как он обладает рядом других привлекательных черт. За это приходится платить несколькими лишними строками кода, в котором все точки контейнера либо помещаются в архив, либо выбираются из него:
void CPolygon: :Serialize (CArchiveS ar) {
//====== Если идет запись в архив,
if (ar. IsStoring() }
{
//=== то последовательно переносим туда все данные
m « m_nPenWidth « m_PenColor « m_BrushColor « m_Points. size () « m_ptLT.x « m_ptLT.y « m_ptRB.x « m_ptRB.y;
for (UINT i=0; i
m « m_Points [i] .x « m_Points [i] . y;
}
else
{
//=== При чтении из архива меняем направление обмена
UINT size;
m » m_nPenWidth » m_PenColor » m_BrushColor
» size » m_ptLT.x » m_ptLT.y
» m_ptRB.x » m_ptRB.y;
//====== Заново создаем контейнер точек полигона
m_Points . clear ( ) ;
while (size--)
{
double x, y;
m » x » y;
m_Points. oush back (CDPoint (x, v) ) ;
}
}
}
Ниже приведена функция рисования полигона в переданный ей в качестве параметра контекст устройства. Второй параметр является флагом, который задает способ заливки полигона. В операциях визуального редактирования, которое мы введем позже, полигон должен временно терять свой цвет, для того чтобы не было мелькания при частых перерисовках.
Напомним, что полигон хранит World-координаты всех своих точек в контейнере m_Points. Переход к Page-координатам производится с помощью функции MapToLogPt, которую мы еще должны разработать и поместить в класс документа. Двигаясь далее по коду функции Draw, мы видим, как объект настраивает контекст устройства с помощью своих личных атрибутов и изображает себя в этом контексте:
void CPolygon::Draw (CDC *pDC, bool bContour)
{
//====== Размер контейнера World-координат точек
UINT nPoints = m_Points.size();
if (!nPoints) return;
//====== Временный массив логических координат точек
CPoint *pts = new CPoint[nPoints];
//====== Преобразование координат
for (UINT i=0; KnPoints; i++)
pts[i] = m_pDoc->MapToLogPt(m_Points[i]);
pDC->SaveDC();
CPen pen (PS_SOLID,m_nPenWidth,m_PenColor);
pDC->SelectObject(Spen);
CBrush brush (bContour ? GetSysColor(COLOR_WINDOW) : m_BrushColor);
pDC->SelectObject(ibrush);
//====== Полигон изображается в предварительно
//====== подготовленном контексте устройства
pDC->Polygon(pts, nPoints);
//====== Освобождаем массив
delete [] pts;
pDC->RestoreDC(-1);
}
Настройка стартового кода
Просмотрите плоды работы мастера в окне Class View. С помощью контекстного меню задайте в этом окне режим просмотра Sort By Type, так как он компактнее, а классов у нас будет достаточно много. Приятным моментом является то, что класс CRightView теперь действительно потомок CScrollView, как мы это определили в окне мастера. В сходной ситуации Visual Studio 6 отказывалась менять родителя, и это приходилось делать вручную. Отметьте также, что во всех отношениях стартовые заготовки Studio.Net 7.0 более компактны, чем их прототипы Visual Studio 6. Тем не менее в них есть лишние детали, которые я с неизменным упорством убираю. Так, каждое из двух представлений имеет по две версии метода GetDocument. Один работает в отладочной (debug) версии проекта, а другой — в окончательной (release). Класс CLef tview, который будет демонстрировать файловое дерево, не нуждается в поддержке вывода на принтер, как и представление CRightView, которое предполагается использовать для предварительного просмотра содержимого файлов документов. Виртуальную функцию preCreateWindow мы также не будем использовать в некоторых классах. То же следует сказать о наследии класса CObject: функциях Assertvalid и Dump. Об особой культуре их использования я говорил в предыдущей книге (Visual C++6 и MFC, «Питер», 2000), а здесь просто рекомендую молча убрать их из всех классов. Если возникнет необходимость вывести в окно Debug отладочную информацию, то можно обойтись без этих функций и в любом методе класса с успехом пользоваться глобально определенным объектом afxDump.Обычно, перед тем как приступить к разработке приложения, я провожу генеральную чистку стартовой заготовки. При выбрасывании лишнего кода, как и при прополке, важно не забывать о корнях. Удалять функцию следует как в срр-файле (реализации класса), так и в h-файле (интерфейса класса). При этом удобной оказывается команда, а точнее ее аналог в виде кнопки на инструментальной панели Edit > Find and Replace > Find in Files. Попробуйте использовать ее для того, что бы найти и удалить с корнем все версии функции GetDocument. Убирайте объявления и тела этой функции, но не ее вызовы. Затем в h-файлы классов CLef tview и CRightview и только в них вставьте такую достаточно надежную версию этой функции:
CTreeDoc* GetDocument()
{
return dynamic_cast
}
Замены такого рода, когда в h-файл вставляется код, а не только декларации, сопряжены с некоторыми неожиданными сообщениями со стороны компилятора. Здесь важно проявить терпение и не опускать руки раньше времени. Если вы правильно сделали замены, то после компиляции проекта получите предупреждение и сообщение об ошибке. С предупреждением справиться просто, если посмотреть справку по его коду (С4541). Выяснится, что для использования информации о типе указателей на этапе выполнения (run-time type information, которой пользуется выражение dynamic_cast
Аббревиатура /GR соответствует опции, задаваемой в командной строке компилятора. После повторной компиляции предупреждения исчезнут, однако ошибка останется. В такие моменты важно обратить внимание на имя файла, при компиляции которого была обнаружена ошибка. В нашем случае — это TreeFrm.cpp. Раскройте этот файл и просмотрите его начало, где стоят директивы #include. Сбой произошел в месте включения файла #include "Lef tview.h". Именно в него мы вставили новое тело функции GetDocument. Компилятор сообщает, что при анализе строки
return dynamic_cast
он обнаружил неверный тип для преобразования (invalid target type for dynamic_ cast). Но тип CTreeDoc* (указатель на класс документа) задан верно. Проблема всего лишь в том, что компилятор пока не знает о том, что CTreeDoc происходит от известного ему класса CDocument. Решение этой проблемы — вставить директиву #include "TreeDoc.h" перед директивой #include "Lef tview.h". В сложных проектах, состоящих из множества файлов, неверная последовательность включения файлов заголовков может привести к дополнительной головной боли. Для выявления причины отказа в таких случаях нужен серьезный анализ этой последовательности.Теперь, запустив приложение, вы должны увидеть заготовку приложения, которое соответствует выбору (флажку) Windows Explorer, сделанному нами в окне мастера AppWizard. Мы имеем два окна, разделенных перегородкой (split bar). Левое окно (рапе) предстоит наполнить ветвями файлового дерева, а в правом — показывать в виде «картинок» файлы документов приложения, обнаруженные в текущей папке — той папке, которая выбрана в левом окне, — дереве файлов. Возвращаясь к сокращениям кода стартовой заготовки, отметим, что многие файлы, будучи уменьшенными в объеме, значительно выигрывают в читабельности и выглядят не так страшно для новичков. В качестве примера приведем текст файла TreeFrm.h после указанной операции1:
class CTreeFrame : public CMDIChildWnd
{
DECLARE_DYNCREATE (CTreeFrame)
public:
CTreeFrame();
virtual ~CTreeFrame();
//====== Создание панелей расщепленного (split) окна
virtual BOOL OnCreateClient(LPCREATESTRUCT Ipcs,
CCreateContext* pContext);
virtual BOOL PreCreateWindow(CREATESTRUCT& cs) ;
protected:
//====== Объект для управления расщепленным окном
CSplitterWnd m_wndSplitter;
DECLARE_MESSAGE_MAP() };
Кроме методов, рассмотренных выше, мы убрали за ненадобностью метод GetRightPane, который добывает адрес представления, расположенного в правой части (рапе) расщепленного окна. Аналогичной редакции (редукции) подвергся и файл Lef tview.h, который, тем не менее, справляется с начальной задачей — показ пустого окна, и в редуцированном виде. Однако этот класс необходимо начать развивать уже сейчас, придавая ему способность управлять деревом файлов. Введите в него объявления новых данных и методов так, чтобы файл LeftView.h приобрел вид:
#pragma once
class CTreeDoc; // Упреждающее объявление
class CLeftView : public CTreeView
{
protected:
//====== Ссылка на объект элемета управления деревом
CTreeCtrlS m_Tree;
//====== Список значков узлов дерева
CImageList *m_pImgList;
CLeftView() ;
virtual void OnlnitialUpdate();
DECLARE_DYNCREATE(CLeftView)
public:
virtual ~CLeftView(); CTreeDoc* GetDocument()
{
return dynamic_cast
}
//====== Выбор системных значков
void GetSysImgList ();
//====== Вставка нового узла (ветви)
void AddltemfHTREEITEM h, LPCTSTR s) ;
//====== Поиск своих документов
void SearchForDocs(CString s) ;
//====== Проверка отсутствия файлов
bool NotEmpty(CString s);
//====== Вычисляет полный путь текущего узла дерева
CString GetPath (HTREEITEM hCur);
DECLARE_MESSAGE_MAP()
};
Мы не собираемся поддерживать вывод на принтер, поэтому в файле реализации класса CLef tview (LeftView.cpp) уберите из карты сообщений класса все макросы, связанные с печатью. Удалите также заготовки тех функций, прототипы которых удалили в файле интерфейса класса (LeftView.h). Это функции PreCreateWindow, OnPreparePrinting, OnBeginPrinting, OnEndPrinting. AssertValid, Dump, GetDocument. Кроме директив препроцессора в файле должен остаться такой код:
IMPLEMENT_DYNCREATE(CLeftView, CTreeView) ,
BEGIN_MESSAGE_MAP(CLeftView, CTreeView) END_MESSAGE_MAP()
CLeftView::CLeftView(){} CLeftView::~CLeftView(){}
void CLeftView: : OnlnitialUpdate {}
{
CTreeView::OnInitialUpdate();
}
Аналогичные упрощения рекомендуем проделать и в классе CRightView. Теперь приступим к анализу и развитию кода класса CLeftView. Внутри каждого объекта класса, производного от CTreeView, содержится объект класса CTreeCtrl, ссылку на который мы объявили в классе CLef tview. Как вы знаете (из курса ООП), единственным способом инициализировать ссылку на объект вложенного класса является ее явная инициализация в заголовке конструктора объемлющего класса. Поэтому измените тело конструктора (в файле LeftView.cpp) так, чтобы он был:
CLeftView::CLeftView()
{
: m Tree(GetTreeCtrl())
// Пустое тело конструктора
}
Метод GetTreeCtrl класса cireeView позволяет добыть нужную ссылку, а вызов конструктора mjrree (GetTreeCtrl ()) инициализирует ее. Теперь мы будем управлять деревом на экране с помощью ссылки m_Tree. Начальные установки для дерева производятся в уже существующей версии виртуальной функции OnlnitialUpdate:
::SetWindowLongPtr (m_Tree.m_hWnd, GWL_STYLE,
::GetWindowLong(m_Tree.m_hWnd, GWL_STYLE)
| TVS_HASBUTTONS | TVS_HASLINES
| TVS_L1NESATROOT | TVS_SHOWSELALWAYS);
Вставьте эту строку в тело OnlnitialUpdate после строки с вызовом родительской версии. Функция SetWindowLongPtr имеет универсальное употребление. Она позволяет внести существенные изменения в поведение приложения, например, с ее помощью можно изменить адрес оконной процедуры или стиль окна. Второй параметр определяет одну из 9 категорий изменений. Задание индекса GWL_STYLE указывает системе на желание изменить стиль окна. Симметричная функция GetWindowLong позволяет добыть переменную, биты которой определяют набор действующих стилей. С помощью побитовой операции ИЛИ мы добавляем стили, специфичные для окна типа Tree view. Префикс TVS означает Tree view styles, а префикс GWL — GetWindowLong. Смысл используемых констант очевиден. Если нет, то он легко выясняется с помощью эксперимента. Вы можете вставить, вслед за обсуждаемой строкой кода, такую:
m_Tree.Insertltem("Item", 0, 0);
и запустить приложение. Несмотря на отсутствие тел новых методов, объявленных в интерфейсе класса, вы увидите одну ветвь дерева с именем «Item».
Немодальный диалог
В предыдущем разделе мы научились редактировать данные документа, воздействуя мышью непосредственно на их представление, то есть облик документа, на экране монитора. Это довольно грубый, но быстрый и эффективный способ, позволяющий получить заготовку некоторой геометрии конструкции, которую впоследствии можно довести до желаемого состояния с помощью таблиц (элементов управления типа grid) или обычных окон редактирования. В практике проектирования геометрии устройств или описания геометрии расчетной области часто используют некоторые стандартные заготовки, которые служат отправной точкой для дальнейшей детализации и усложнения геометрии. Такие заготовки целесообразно выбирать с помощью окон диалога, работающих в немодальном режиме и зачастую называемых Toolbox-window. В них пользователь может выбрать одну из стандартных заготовок геометрии устройства или изменить атрибуты текущей. Создайте с помощью редактора диалогов Studio.Net форму диалога, которая выглядит так, как показано на рис. 5.5. Типы элементов управления, размещенных в окне диалога, и их идентификаторы сведены в табл. 5.1.
Рис. 5.5. Вид окна диалога
Таблица. 5.1 Идентификаторы элементов управления
Элемент | Идентификатор | ||
Диалог | 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 |
Для трех кнопок (TRI, PENT и STAR) установите стиль Owner draw, так как это будут не стандартные кнопки, а кнопки с изображениями, управляемые классом CBitmapButton. Для ползунков установите следующие стили: Orientation: Horizontal, TickMarks: True, AutoTicks: True, Point: Top/Left.
Для управления диалогом необходимо создать новый класс. Для этого можно воспользоваться контекстным меню, вызванным над формой диалога.
Просмотрите объявление класса CPolyDlg, которое должно появиться в новом окне PolyDlg.h. Как видите, мастер сделал заготовку функции DoDataExchange для обмена данными с элементами управления на форме диалога. Самих функций обмена типа DDX_ еще нет, но мы их создадим немного позже.
Нестандартные элементы управления
Рассмотрим, как создаются элементы управления, имеющие индивидуальный нестандартный облик. Сравнительно новым подходом в технологии создания таких элементов является обработка подходящего сообщения не в классе родительского окна, а в классе, связанном с элементом управления диалога. Такая возможность появилась в MFC начиная с версии 4.0, и она носит название Message Reflection. Элементы управления Windows посылают уведомляющие сообщения своим родительским (parent) окнам. Например, многие элементы, в том числе и Edit controls, посылают сообщение WM_CTLCOLOR, позволяющее родительскому окну выбрать кисть для закраски фона элемента. В версиях MFC (до 4.0), если какой-либо элемент должен выглядеть не так, как все, то эту его особенность обеспечивал класс родительского окна, обычно диалог. Теперь к старому механизму обработки уведомляющих сообщений от дочерних (child) элементов добавился новый, который позволяет произвести обработку уведомляющего сообщения в классе самого элемента. Уведомляющее сообщение как бы отражается (reflected) назад в класс дочернего окна элемента управления. Мы собираемся использовать нестандартные окна редактирования (Red, Green, Blue и Color), с тем чтобы они следили за изменением цвета, отражая текущий выбор как в числовом виде, так и в виде изменяющегося цвета фона своих окон. Эту задачу можно выполнить, создав класс (назовем его cclrEdit), производный от CEdit, и введя в него обработку отражаемого сообщения =WM CTLCOLOR.
Применяя уже известный вам подход, создайте класс 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 в классе нестандартного окна редактирования.
Найдите заготовку тела функции ctlColor в файле PolyDlg.cpp и вставьте в нее следующие коды:
HBRUSH CClrEdit::CtlColor(CDC* pDC, UINT nCtlColor)
{
pDC->SetTextColor (m_clrText); // Цвет текста
pDC->SetBkColor (m_clrBk); // Цвет подложки текста
return m_brBk; // Возвращаем кисть
}
Создайте тело вспомогательной функции ChangeColor, которую мы будем вызывать в те моменты существования диалога, когда пользователь изменяет значения элементов управления цветом:
void CClrEdit::ChangeColor(COLORREF clr)
{
//====== Цвет текста - инвертирований цвет фона
m_clrText = ~clr & Oxffffff;
m_clrBk = clr;
//====== Создаем кисть цвета фона
m_brBk.DeleteObject();
m_brBk.CreateSolidBrush (clr);
Invalidate ();
}
Главным управляемым параметром является кисть (m_brBk), которую в ответ на отраженное сообщение =WM_CTLCOLOR надо возвратить каркасу приложения. Попутно мы изменяем цвет текста (setTextColor) и его подложки (setBkColor). Чтобы понять, что такое подложка текста, при отладке временно закомментируйте строку
pDC->SetBkColor (m_clrBk);
При изменении (инвертировании) цвета текста мы вынуждены обнулять четвертый байт переменной m_clrText. В более старых версиях Windows это действие было лишним. Теперь четвертый байт используется для задания степени прозрачности при воспризведении растровых изображений. Если он не равен нулю, то инвертирование цвета не проходит. Первые три байта, как вы помните, задают три компонента (red, green, blue).
Изменение цвета пользователем с помощью элементов управления будет мгновенно отслеживаться в четырех полях диалога (три компонента цвета и суммарный цвет в окне Color). Так как мы хотим отследить изменение цвета и в окне представления, управляемого классом CDrawView, то мы добываем адрес родительского oкna.(GetParent) и вызываем вспомогательную функцию UpdateDrawView.
Немодальный режим работы Особенность
CPolyDlg::OnClickedOk(void){
//=== Запоминаем факт отсутствия диалога в документе
m_pDoc->m_pPolyDlg = 0;
//====== Родительская версия вызовет DestroyWindow
CDialog::OnOK();
//====== Мы освобождаем память
delete this;
}
Вызов диалога производится в ответ на команду Edit > Poly Color, которая уже присутствует в меню IDR_DrawTYPE. Введите в класс CTreeDoc обработчик этой команды и наполните его кодами, как показано ниже:
void CTreeDoc::OnEditPolycolor(void)
{
//====== Если диалог отсутствует
if (!m_pPolyDlg)
{
//====== Создаем его в две ступени
m_pPolyDlg = new CPolyDlg(this);
m_pPolyDlg->Create(IDD_POLYCOLOR) ;
}
else
//===== Иначе делаем активным его окно
m_pPolyDlg->SetActiveWindow();
}
Здесь использован указатель на объект диалогового класса, который необходимо ввести в число public-данных класса CTreeDoc (CPolyDlg *m_pPolyDlg;) и обнулить в конструкторе документа (m_pPolyDlg = 0;). Сделайте это, а также введите в файл реализации класса CTreeDoc вспомогательную функцию UpdateDrawView:
void CTreeDoc::UpdateDrawView()
{
//====== Добываем адрес нужного представления
CDrawView *pView = dynamic_cast
(GetView(SCDrawView::classCDrawView));
//====== и просим его перерисоваться с учетом изменений
if (pView)
pView->Invalidate();
}

Рис. 5.6. Управление с помощью немодального диалога
Изменения такого рода, как вы уже догадались, влекут за собой достаточно много ошибок на стадии компиляции, если не уделить внимания проблеме видимости классов. Так, надо вставить упреждающее объявление (class CPolyDlg;) в файл с интерфейсом документа и директиву #include "PolyDlg.h" в файл с его реализацией. Кроме того, при работе с диалогом в немодалыюм режиме надо помнить о том, что для его окна свойство Visible должно быть установлено в True. По умолчанию это свойство выключено, так как при запуске диалога в модальном режиме диалог сначала невидим, но .затем функция DoModal вызывает showWindow с параметром SW_SHOW, что активизирует окно, делая его видимым. Мы тоже можем поступить так же, вставив аналогичный вызов после вызова функции Create, но проще сразу установить для диалога (в категории Behavior окна Properties) свойство Visible.
В настоящий момент приложение может быть запущено и при условии отсутствия ошибок протестировано. Команда запуска диалога должна быть доступна, только когда активно окно CDrawFrame, или, точнее, фокус ввода принадлежит представлению, управляемому классом CDrawView. Проверьте все варианты запуска диалога: с помощью команды меню или кнопки на панели инструментов. Проверьте также возможность перевода фокуса в любое из представлений документа при наличии окна диалога (рис. 5.6).
Нестандартные кнопки Кнопкам управления
);m_cPent.Autoload (IDC_PENT, this);
m_cStar.AutoLoad (IDC_STAR, this);
CDialog::OnlnitDialog{);
//====== Установка диапазона ползунков
m_rSlider.SetRange (0, 255);
m_gSlider.SetRange (0, 255);
m_bSlider.SetRange (0, 255);
//====== Установка цены деления ползунков
m_rSlider.SetTicFreq (50);
m_gSlider.SetTicFreq (50);
m_bSlider.SetTicFreq (50);
//=== Вызов обработчиков для начальной
//=== закраски окон и установки ползунков OnChangeRedO ;
OnChangeGreen();
OnChangeBlue ();
return TRUE;
}
В Visual Studio 6 эта функция создавалась как обработчик сообщения WM_INITDIALOG, здесь в Studio.Net 7.0 я не обнаружил сообщения с таким именем в списке сообщений диалогового класса. Однако в списке Overrides присутствует строка с именем OnlnitDialog. В принципе рассматриваемая функция и в Visual Studio 6 имеет прототип virtual BOOL OnlnitDialog, но classWizard 6-й версии причисляет ее к функциям-обработчикам сообщений. Характерным моментом является также то, что прототип функции в Studio.Net изменился и стал BOOL OnlnitDialog (void);. Возвращаясь к диалоговому классу, заметим, что обращение к методам класса CTreeDoc требует включить традиционную строку
#include "TreeDoc.h"
в список директив препроцессора файла PolyDlg.cpp.
Обработка сообщений от элементов
CPolyDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar){
//====== Неинтересное для нас сообщение
if (nSBCode==SB_ENDSCROLL)
return;
//====== Устанавливаем флаг сообщений от ползунков
m_bScroll = true;
//====== Узнаем идентификатор активного ползунка
switch(GetFocus()->GetDlgCtrlID())
{
case IDC_RSLIDER:
//====== Считываем текущую позицию движка
m_nRed = m_rSlider.GetPos();
//====== Синхронизируем поле, редактирования
SetDlgltemlnt(IDC_RED, m_nRed);
break;
case IDC_GSLIDER:
m_nGreen = m_gSlider.GetPos();
SetDlgltemlnt(IDC_GREEN, m_nGreen);
break; case IDC_BSLIDER:
m_nBlue = m_bSlider.GetPos() ;
SetDlgltemlnt(IDC_BLUE, m_nBlue);
break;
}
//====== Снимаем флаг сообщений от ползунков
m_bScroll = false;
}
Сообщения от всех ползунков обрабатываются в одной функции. Идентификатор активного ползунка определяется путем последовательного вызова двух функций GetFocus и GetDlgctrliD, являющихся методами класса cwnd.
Флаг сообщений от ползунков (m_bScroll) понадобился нам для того, чтобы при синхронизации элементов управления не происходили повторные вызовы функций-обработчиков. Дело в том, что при изменении позиции ползунка мы должны привести в соответствие окно редактирования, а при ручном изменении числа в окне редактирования мы должны синхронизировать позицию ползунка. Но сообщение EN_CHANGE посылается как при ручном изменении, так и при программном изменении с помощью функции SetDlgltemlnt. Проследим цепь таких событий: пользователь подвинул движок ползунка, мы вызываем SetDlgltemlnt, она провоцирует посылку сообщения EN_CHANGE, а обработчик этого сообщения корректирует положение ползунка, которое и без того верно.
Введите в класс диалога реакции на уведомления EN_CHANGE от четырех элементов IDC_PEN, IDC_RED, IDC_GREEN И IDC_BLUE. Вы помните, что это надо делать с помощью кнопки Events в окне Properties. Вставьте коды в остовы функций обработки, как показано ниже:
void CPolyDlg::OnChangePen(void)
{
BOOL bSuccess; //====== Попытка преобразовать в число
UINT nSize = GetDlgltemlnt(IDC_PEN, SbSuccess, FALSE);
if (bSuccess && nSize < 101)
{
m_nPen = nSize;
m_pDoc->m_Poly-m_nPenWidth = m_nPen;
m_pDoc->UpdateDrawView();
}
}
Отметьте, что здесь мы намеренно не пользуемся функцией UpdateData, которая провоцирует обмен данными сразу со всеми полями окна диалога, так как хотим показать более экономный способ выборочного (целевого) обмена с помощью функции GetDlgltemlnt. Правда, при таком подходе не работают функции проверки данных типа DDV_ и приходится производить проверку самостоятельно:
void CPolyDlg::OnChangeRed(void) {
//====== Если сообщение спровоцировано ползунком,
//====== то обходим коды его синхронизации
if (!m_bScroll)
{
m_nRed = GetDlgltemlnt(IDC_RED, 0, FALSE);
m_rSlider.SetPos(m_nRed);
//====== Изменяем цвет фона окна редактирования
m_cRed.ChangeColor(RGB(m_nRed, 0, 0));
//====== Корректируем интегральный цвет
UpdateColor();
}
void CPolyDlg::OnChangeGreen(void)
{
if (!m_bScroll)
{
m_nGreen = GetDlgltemlnt(IDC_GREEN, 0, FALSE), m gSlider.SetPos(m_nGreen);
m_cGreen.ChangeColor(RGB(0, m_nGreen, 0)); UpdateColor ();
}
void CPolyDlg::OnChangeBlue(void)
{
if (!m_bScroll)
{
m_nBlue = GetDlglteralnt(IDC_BLUE, 0, FALSE);
m_bSlider.SetPos(m_nBlue);
}
m_cBlue.ChangeColor(RGB(0, 0, m_nBlue));
UpdateColor ();
}
Введите тело вспомогательной функции, которая вычисляет интегральный цвет и вносит изменения, перекрашивая окно диалога IDC_COLOR, и с помощью документа текущий полигон в окне CDrawView:
void CPolyDlg::UpdateColor()
{
COLORREF clr = RGB (m_riRed,m_nGreen,m_nBlue) ;
m_cColor.ChangeColor(clr) ;
m_pDoc->m_Poly.m_BrushColor = clr;
m_pDoc->UpdateDrawView();
}
С помощью Studio.Net введите в класс диалога реакции на уведомляющие сообщения (BN_CLICKED) о нажатии кнопок выбора стандартных геометрий для полигонов (IDCJTRI, IDC_PENT и IDC_STAR). В них мы с помощью техники обратного указателя вновь обращаемся к документу и используем его данные и методы для замены координат точек текущего полигона:
void CPolyDlg::OnClickedTri(void)
{
m_pDoc->m_Poly.MakeTria() ;
m_pDoc->UpdateDrawView() ;
}
void CPolyDlg::OnClickedPent(void)
{
m_pDoc->m_Poly.MakePent() ;
m_pDoc->UpdateDrawView() ;
}
void CPolyDlg::OnClickedStar(void)
{
m_pDoc->m_Poly.MakeStar() ;
m_pDoc->UpdateDrawView();
}
Измените тело конструктора диалогового класса, с тем чтобы при открытии диалога он смог запомнить обратный указатель (адрес документа) и все его элементы были правильно инициализированы:
CPolyDlg::CPolyDlg(CTreeDoc* p)
: CDialog (CPolyDlg::IDD, 0)
{
m_pDoc = p;
m_nPen = p->m_Poly.m_nPenWidth;
//====== Расщепляем цвет фона текущего полигона
COLORREF brush = p->m_Poly.m_BrushColor;
m_nRed = GetRValue(brush); // на три компонента
m_nGreen = GetGValue(brush);
m_nBlue = GetBValue(brush) ;
m_bScroll = false; // Ползунки в покое
Обращаемся к операционной системе
Теперь, когда вы научились управлять формой дерева, мы продолжим развитие приложения. Используя клавишу Delete, удалите все ресурсы типа Bitmap. Удалите также глобальное объявление структуры TVINSERTSTRUCT. Теперь мы покажем, что можно обходиться и без ее помощи. Уберите весь учебный код, следующий после строки m_plmgList = new CImageList, и вставьте новый, так, чтобы функция приобрела вид:void CLeftView::OnInitialUpdate()
{
CTreeView::OnInitialUpdate();
::SetWindowLongPtr(m_Tree.m_hWnd, GWL_STYLE, GetWindowLong(m_Tree.m_hWnd, GWL_STYLE)|TVS_HASLINES I TVS_HASBUTTONSITVS_LINESATROOT|TVS_SHOWSELALWAYS);
//====== Создаем новый список изображений
m_pImgList = new CImageList;
//====== Связываем его с системным списком изображений
GetSvsImqList () ;
//====== Получаем имена логических дисков
char s [1024] ;
DWORD size = ::GetLogicalDriveStrings (1024, s);
if (Isize) // В случае отказа
return; // уходим молча
//=== Сканируем текст и вставляем новые узлы дерева
for (char *pName = s; *pNarae; pName += strlen(pName)+1)
Addltem (TVI_ROOT, pName);
}
Функция GetSysimgList, которую мы создадим чуть позже, получает от системы список системных значков и связывает его с деревом. Начать показ файлового дерева мы решили с демонстрации всех логических дисков, имеющихся в операционной системе в данный момент. API-функция GetLogicalDriveStrings заполняет строку текста, в которую она помещает перечень всех присутствующих в операционной системе логических дисков. Строка имеет особый формат: она состоит из нескольких подстрок, завершающихся нулем, например:
a:\0c:\0d:\00
Обратите внимание на то, что признаком конца перечня являются два нулевых байта. Первый завершает подстроку, а второй — всю строку. Используя эту особенность, мы создали цикл for (), в котором подстроки — имена логических дисков, сначала выявляются, а затем используются для вставки в дерево узлов, соответствующих логическим дискам. Функция Addltem, которую создадим позже, определяет индекс значка, соответствующего вставляемой сущности (диск, папка или файл), и создает в дереве новый узел с соответствующим ему изображением.
Теперь займемся созданием вспомогательных функций, которые понадобились при разработке функции OninitialUpdate. Введите в файл LeftView.cpp реализацию функции GetSysimgList, объявление которой уже существует в файле интерфейса LeftView.h класса CLef tview:
void CLeftView::GetSysImgList()
{
SHFILEINFO info;
// Попытка получить описатель системного списка значков
HIMAGELIST hlmg = (HIMAGELIST)
::SHGetFilelnfо("С:\\",0, Sinfo,sizeof (info), SHGFI_SYSICONINDEX | SHGFI_SMALLICON);
//=== Приписываем описатель системного списка
//=== изображений объекту CImageList
if (Ihlmg !m_pImgList->Attach(hlmg))
{
MessageBox(0,"He могу получить System Image List!");
return; }
//=== Связывание списка с элементом управления деревом m_Tree.SetlmageList(m_pImgList, TVSIL_NORMAL);
}
Функция SHGetFilelnfo позволяет получить информацию о каком-либо объекте файловой системы. Последний параметр уточняет смысл вопроса. Определяем его с помощью битовых констант SHGFI_SYSICONINDEX и SHGFI_SMALLICON, которые означают, что мы интересуемся индексами значков в системном списке и нам нужны маленькие значки. Вы помните, что Windows поддерживает значки двух типов: большие (32x32) и маленькие (16x16). Результатом вызова функции будет описатель (handle) всего списка значков, который мы затем должны связать с элементом m_Tree. Но сначала требуется прикрепить (attach) Windows-описатель списка к объекту класса CimageList, адрес которого мы храним в переменной m_pImgList.
Понятие прикрепить описатель (attach a handle) вы будете встречать достаточно часто, программируя в рамках MFC, но значительно реже, чем разработчики, базирующиеся на платформе SDK (Software Development Kit), которые не пользуются классами MFC. Вместо этого они используют многочисленные структуры и прямо вызывают функции API из программы на языке С или C++. При этом им иногда приходится писать в 5-10 раз больше кода. Итак, понятие прикрепить описатель означает примерно следующее: дать объекту класса ту функциональность, которой обладает Windows-объект, обычно описываемый структурой и адресуемый с помощью описателя (handle). Внутри многих классов MFC скрыто существуют Windows-описатели, которые должны быть правильно инициализированы. Часто, но не всегда, это делается без нашего участия. Иногда мы должны предпринять какие-то действия для инициализации описателя. В данном случае это можно сделать прямым присвоением, например m_pimgList->m_hImageList = himg; но такой способ менее надежен, так как в нем непосредственно запоминается какой-то адрес памяти. Содержимое по этому адресу система может изменить в результате наших же манипуляций с объектами, и тогда мы получим проблему под названием «Irreproducible Bug» (невоспроизводимая ошибка). Точнее будет сказать трудновоспроизводимая ошибка — самый неприятный тип ошибок, для борьбы с которыми идут в ход все средства (даже AssertValid и Dump). Значительно надежнее использовать метод Attach класса CimageList, так как в этом случае система будет следить за перемещениями структур, адресуемых описателем. При этом работает класс CHandleMap и его метод SetPermanent, которые, к сожалению, не документированы.
Связывание списка с объектом m_Tree производит функция SetlmageList, последний параметр которой (TVSIL_NORMAL) говорит о том, что тип списка обычный, то есть состоит из двух изображений. Альтернативным выбором является TVSIL_STATE, справку о нем вы получите самостоятельно, если захотите. Поместите следующий код в файл LeftView.cpp. Он вставляет в дерево новый элемент с изображением, которое ему соответствует:
void CLeftView::AddItem (HTREEITEM h, LPCTSTR s)
{
SHFILEINFO Info;
int len = sizeof(Info);
//=== Добываем изображение (маленький значок)
::SHGetFileInfo (s, 0, SInfo, len, SHGFI_ICON
| SHGFI_SMALLICON); int id = Info.ilcon;
//=== Добываем изображение в выбранном состоянии
::SHGetFileInfo (s,0,Slnfo,len,
SHGFI_ICON | SHGFI_OPENICON | SHGFI_SMALLICON);
int idSel = Info.ilcon;
//====== Копируем параметр в рабочую строку
CString sName(s);
//=== Отсекаем лишние символы (сначала в конце строки)
if (sName.Right(1) == '\\')
sName.SetAt (sName.GetLength() - 1, '\0');
//====== Затем в начале строки
int iPos = sNarae.ReverseFind('\\') ;
if (iPos != -1)
sName = sNarne.Mid(iPos + 1) ;
//=== Вставляем узел в дерево
HTREEITEM hNew = m_Tree.InsertltemfsName,id,idSel,h);
//====== Вставляем пустой узел
if (NotErapty(s))
m_Tree.Insertltem("", 0, 0, hNew);
}
Функция SHGetFilelnf о вызывается дважды, так как от системы надо получить два индекса изображений: для объекта файловой системы в обычном состоянии и для него же в выбранном состоянии. Метод Insertltem класса CTreeCtrl вставляет узел в дерево. Его параметры задают:
Вставляемый в дерево логический диск надо проверить на наличие вложенных сущностей и вставить внутрь данного узла дерева хотя бы один элемент, когда диск не пуст. Если этого не сделаеть, то в дереве не будет присутствовать маркер (+), с помощью которого пользователь раскрывает узел.
При проверке диска ( функция NotEmpty) мы не сканируем его далеко вглубь, а просто проверяем на наличие хотя бы одной папки. Если диск имеет хотя бы одну папку, то вставляем внутрь соответствующего ей узла пустой элемент (Insertltem ("", 0, 0, h)), который дает возможность впоследствии раскрыть (expand) данный узел. Затем, когда пользователь действительно его раскроет, мы обработаем это событие и удалим пустой элемент. Вместо него наполним раскрытую ветвь реальными сущностями. Этот прием обеспечивает постепенное наполнение дерева по сценарию, определяемому действиями пользователя.
Операция отсечения лишних символов нам понадобилась для того, чтобы из длинного файлового пути выделить только имя папки, которое должно появится в дереве справа от bitmap-изображения объекта — узла дерева. Мы решили показывать в дереве, в левом окне приложения, только папки. Файлы этих папок будут изображены в виде картинок в другом, правом, окне. Картинкой я называю содержимое документа в виде его чертежа — многоугольника (для простоты). Показывать будем только те файлы, которые соответствуют документам нашего приложения. Если вы помните, они должны иметь расширение mgn, как это было определено на этапе работы с мастером AppWizard.
При усечении строки неоходимо использовать знание структуры файлового пути и методы класса cstring. Сначала отсекаем символ ' \' справа от имени папки, затем все символы слева от него. Существует и другой способ, использующий функцию _splitpath, справку по которой я рекомендую получить самостоятельно. В настоящий момент развития приложения строка sName может содержать только одно из имен логических дисков и большая часть кода работает вхолостую, но чуть позже, когда мы будем иметь дело с длинными файловыми путями, он заработает полностью.
Для того чтобы оживить дерево в его начальном состоянии, осталось добавить код функции NotEmpty, которая проверяет текущий узел (файловый адрес папки) на наличие в нем вложенных папок и возвращает true в случае успеха и false, в случае если папка пуста или в ней присутствуют только файлы.
В библиотеке MFC имеется класс CFileFind, который умеет обнаруживать в папке любые объекты файловой системы. Если объекту такого класса, который обнаружил объект «точка», задать вопрос isDirectory (), то он ответит утвердительно. Тот же ответ будет получен и на другой вопрос isDots (). Другим объектам файловой системы, настоящим папкам и файлам, соответствуют другие ответы на эти же вопросы. Папки отвечают на первый вопрос утвердительно, а на второй отрицательно. Простым файлам нет смысла задавать второй вопрос, так как они отвечают отрицательно на первый. Для них актуален другой вопрос isHidden (), на который утвердительно отвечают файлы с Windows-атрибутом hidden. Его можно использовать для управления показом файлов. В случае если папка содержит только такие файлы, то мы будем считать, что она пуста. Если в папке есть и другие, то в их числе могут быть и mgn-файлы наших документов. В этом случае мы будем считать, что папка не пуста. С учетом сказанного строим алгоритм и функцию проверки файлового адреса:
bool CLeftView::NotErapty(CString s)
{
//====== Параметр s содержит текущий файловый путь
//====== Объект класса, умеющего искать нечто в папке
CFileFind cff;
//====== Дополняем путь маской *.* или \*.*
s += s.Right(l) == '\\' ? "*.*" : "\\*.*";
BOOL bFound = cff.FindFile(s);
//====== Цикл поиска настоящих объектов
while (bFound)
{
bFound = cff.FindNextFile(); //====== Это папка?
if (cff . IsDirectory () && ! cf f. IsDots () )
return true; //====== Это файл?
if (!cff.IsDirectory() SS !cff.IsHidden())
return true;
}
//====== He найдены объекты, достойные внимания
return false;
}
Отметьте, что цикл while не будет продолжительным, так как выход из него происходит при обнаружении первой же настоящей папки или файла. Запустите приложение, устраните возможные ошибки и убедитесь в том, что дерево с изображениями дисков действительно появляется в левом окне. При раскрытии узлов дерева, соответствующих «не пустым» дискам, появляется только одно изображение, которое определяется нулевым индексом системного списка (рис. 5.2). Вы помните, что в «непустые» узлы мы вставляли нулевые элементы. Рекомендуем с

Рис. 5.2. Вид расщепленного окна с файловым деревом
учебными целями ввести исправления и добиться демонстрации не только папок, но и файлов. Убедитесь в том, что различным типам файлов соответствуют разные изображения. Они, как вы помните из третьего урока и знаете из опыта общения с Windows, определены на этапе регистрации значка приложения или его документа.
Обзор функции Initlnstance Внесем
FALSE;}
AfxEnableControlContainer() ;
который представляет собой инициализацию поддержки OLE (Object Linking and Embedding), то его можно убрать, так как наше приложение не будет выполнять функции OLE-сервера или OLE-контейнера. Следующая строка:
SetRegistryKey(_T("Local AppWizard-Generated..."));
представляет собой создание нового ключа в реестре Windows для хранения некоторой информации о нашем приложении. Он действительно будет новым, если вы измените строку текста на имя вашей компании, как это было задумано при разработке функции, или на какое либо другое имя («My Soft»). После запуска приложения можно открыть реестр (в командной строке Windows дайте команду: RegEdit) и отыскать в нем по адресу HKEY_CURRENT_USER\Software вновь созданный ключ My Soft. Записывать информацию по этому ключу вы можете с помощью методов класса cwinApp, от которого происходит наш класс CTreeApp. Например, метод WriteProf ilelnt позволяет записать некое целое значение (value), соответствующее произвольной секции текущего ключа. Для эксперимента вставьте вместо строки SetRegistryKey такие три строки:
SetRegistryKey("My Soft");
WriteProfileStringC'My Data", "My Name","Alex");
WriteProfilelnt("My Data","My Age",54);
Запустите приложение, перейдите в окно реестра, обновите его (View > Refresh), найдите адрес HKEY_CURRENT_USER\Software\My Soft\Tree\My Data, поставьте в него курсор мыши и убедитесь в наличии двух записей, высвечиваемых в правом окне реестра. Удалите из реестра ключ My Soft, если вам нужен реестр, а не свалка мусора (чем он обычно и является). Уберите также учебный код из тела initinstance.
Для того чтобы увидеть, как работает функция LoadStdProf ileSettings, вызов которой виден в теле initinstance, запустите приложение и запишите хотя бы один документ (команда: File > Save). После этого вы можете найти в реестре (не забывайте освежать его) по тому же адресу новую секцию Recent File List, которая содержит запись — полный путь к только что записанному файлу. Параметр функции LoadStdProf ileSettings указывает, сколько записей может содержать список MRU (Most Recently Used) последних документов. Если вы зададите его равным нулю, то список не будет поддерживаться каркасом приложения.
Теперь можно приступить к созданию двух шаблонов документов вместо одного, рассмотренного выше. Для того чтобы задействовать второй шаблон, надо убрать из Initinstance код по созданию шаблона pDocTemplate и вставить вместо него такие строки:
//====== Создаем первый шаблон
m_pTemplTree = new CMultiDocTemplate(IDR_TreeTYPE,
RUNTIME_CLASS(CTreeDoc) ,
RUNTIME_CLASS(CTreeFrame) ,
RUNTIME_CLASS(CLeftView)) ;
//====== Помещаем его в список
AddDocTemplate(m_pTemplTree);
//====== Создаем второй шаблон
m_pTemplDraw = new CMultiDocTemplate(IDR_DrawTYPE,
RUNTIME_CLASS(CTreeDoc),
RUNTIME_CLASS(CDrawFrame),
RUNTIME_CLASS(CDrawView));
//====== Помещаем его в список
AddDocTemplate(m_pTemplDraw);
Второй шаблон тоже помещается в список шаблонов приложения. Каркас приложения устроен так, что теперь каждый раз, когда пользователь будет выбирать команду File > New, будет появляться диалог со списком шаблонов и просить его выбрать шаблон, которому должен соответствовать новый документ. Идентификатор ресурсов !DR_DrawTYPE определяется заранее, то есть в файле resource.h должна быть макроподстановка #def ine, заменяющая этот идентификатор целым положительным числом. Самым простым способом создания нового идентификатора является вызов команды Edit > Resource Symbols. Но этот способ будет некорректным
в нашем случае, так как мы поместили второй шаблон в список шаблонов, ассоциированных
с документами приложения, и его идентификатор должен быть связан с какими-то ресурсами.
Ресурсов, которые связаны со вторым шаблоном, может быть несколько, и мы покажем, как связать с ним значок, меню, панель инструментов и строковый ресурс, расположенный в таблице String Table. Последний является текстовой строкой, которая разбита символами ' \п' на отдельные части — подстроки. Каждая подстрока имеет определенное значение и используется каркасом приложения в разные моменты его жизни. Например, вторая подстрока является корнем для образования имен новых документов, и вы обычно видите ее в заголовке дочернего окна документа. Откройте окно Resource View, раскройте узел дерева под именем String Table и сделайте двойной щелчок на вложенном в него элементе. В таблице строк справа найдите iDR_TreeTYPE. Он идентифицирует комплексную строку:
\nTree\nTree\nTree Files (*.mgn)\n.mgnXnTree.Document\nTree.Document
Если мы поместим в String Table новую строку с идентификатором !DR_DrawTYPE, то при открытии окон документов по шаблону m_pTemplDraw, они будут использовать этот ресурс. При вставке новой строки надо быть внимательным, так как ее индекс должен быть в определенном диапазоне.
Убедитесь в том, что индекс новой строки (видимо, 130) следует за индексом, соответствующим строке IDR_TreeTYPE, при этом строки двух шаблонов стоят рядом. Если индекс новой строки не попал в нужный диапазон, то придется все повторить. Замените поле Caption строкового ресурса IDR_MAINFRAME на Doc Viewer. Это необходимо для того, чтобы пользователь легче воспринял закономерность образования заголовков окон новых документов.
Завершая обзор функции Initinstance, расскажем, что делают остальные функции, вызов которых происходит при инициализации приложения. Вызов
m_pMainWnd->DragAcceptFiles();
с параметром TRUE, заданным по умолчанию, сообщает системе, что главное окно приложения способно обработать сообщение WM_DROPFILES. Благодаря этому пользователь может методом Drag&Drop переместить в открытое окно приложения файл нашего документа (mgn-файл), и он будет обработан командой File > Open. Вызов функции EnableShellOpen делает возможным запуск нашего приложения при двойном щелчке на mgn-файле или его значке (icon), а вызов RegisterShellFileTypes регистрирует новый тип файлов (файлы документов нашего приложения) и действия при его открытии двойным щелчком. Регистрация не производится, если данное расширение (mgn) уже присутствует в базе данных Windows и с ним связано какое-то действие. Например, если мы вместо mgn выберем расширение mag, то наши файлы будут рассматриваться системой как файлы Microsoft Access Diagram Shortcut или как файлы документов приложения ACDSee в зависимости от того, что установлено в системе. Это малоприятная история, выходом из которой, как нам говорят разработчики системы, является возможность задавать файлам документов более длинное расширение. Нет уверенности в том, что это будет хорошим решением, так как вероятность совпадений остается достаточно высокой.
В файле ТгееАрр.срр присутствует также декларация и определение класса CAbout Dig, производного от CDialog и обслуживающего окно простого диалога, ресурс которого (IDD_ABOUTBOX) уже имеется в каркасе приложения. Так как мы не собираемся развивать диалог, то можно убрать класс и все его методы, оставив лишь функцию вызова OnAppAbout, тело которой упрощается до:
void CTreeApp::OnAppAbout()
{
// Класс CDialog справляется с задачей
CDialog(IDD_ABOUTBOX).DoModaK);
}
Окна с геометрией данных Характерный
CWndGeom : public CWnd{
public:
CTreeDoc *m_pDoc;
// Адрес документа (для удобства)
CRightview *m_pView;
// Адрес родительского окна
int m_ID;
// Индекс окна документа в массиве CRect m_Rect;
// Координаты в правом окне
//====== Удобный для нас конструктор
CWndGeom (CRightview *p, int id);
~CWndGeom();
protected: DECLARE_MESSAGE_MAP()
};
В файле реализации класса измените коды конструктора, как показано ниже. Затем с помощью Studio.Net введите в класс реакции на следующие сообщения: WM_PAINT, WM_LBUTTONDOWN и WM_MOUSEMOVE. Цель этих действий такова. При наведении курсора мыши на одно из окон, управляемых классом CWndGeom, оно должно проявить признаки готовности быть выбранным. Для этого рисуем в нем обрамляющий прямоугольник, который исчезает при выходе указателя мыши за пределы окна. Эта функциональность реализуется за счет пары функций
SetCapture - ReleaseCapture. Метод CWnd: : SetCapture захватывает текущее окно как адресат для последующих сообщений мыши независимо от позиции курсора. Поэтому при перемещении курсора мыши можно выйти из пределов клиентской области окна и все равно получить и обработать сообщение им_ MOUSEMOVE. На этом свойстве окна и построен алгоритм его подсветки. Функция ReleaseCapture «освобождает мышь», то есть вновь восстанавливает обычный порядок обработки мышиных сообщений. Мы вызываем функцию после того, как обнаружен факт выхода за пределы окна и снята подсветка, то есть стерт обрамляющий прямоугольник:
CWndGeom::CWndGeom(CRightView *p, int id)
{
//====== Запоминаем адрес родительского окна
m_pView = р;
//====== Запоминаем адрес документа
m_pDoc = p->GetDocument();
//====== и индекс окна в массиве
m_ID = id;
}
void CWndGeom::OnPaint()
{
CPaintDC dc(this);
dc.SetMapMode(MM_ISOTROPIC) ;
//====== Настраиваем логическое окно
dc.SetWindowOrg (m_pDoc->m_szDoc.cx/2, m_pDoc->m_szDoc.cy/2), dc.SetWindowExt(m_pDoc->m_szDoc);
//====== Узнаем текущие размеры окна
GetClientRect(&m_Rect);
int w = m_Rect.Width (), h = m_Rect.Height ();
//====== Настраиваем аппаратное окно
dc.SetViewportOrg (w/2, h/2);
dc.SetViewportExt (w, -h);
//=== Выбираем в контейнере нужный полигон и просим
//=== его изобразить себя в подготовленном контексте m_pDoc->m_Shapes[m_ID].Draw(Sdc);
}
void CWndGeom: :OnLButtonDown (UINT nFlags, CPoint point)
{
//====== Изменяем дежурный полигон
m_pDoc->m_Poly = m_pDoc->m_Shapes [m_ID] ;
//== Если не было CDrawView, то создаем его
if (m_pDoc->MakeView() )
return;
//=== Если он был, то находим его и делаем активным
CView *pView = m_pDoc->GetView (RUNTIME_CLASS (CDrawView) ;
{
(CMDIChildWnd*)pView->GetParentFrame ()
} ->MDIActivate () ;
//====== Перерисовка с учетом изменений
pView->Invalidate ( ) ;
Все «мышиные» сообщения сопровождаются параметрами, которые информируют нас о том, где и как произошло событие. Первый параметр есть набор битов, указывающих, какие виртуальные клавиши были нажаты в момент события. Например, если nFlags содержит бит с именем MK_CONTROL (символическая константа), то это означает, что в момент нажатия левой кнопки мыши также была нажата клавиша Ctrl. Второй параметр содержит координаты (х, у) местоположения курсора в момент события. Они заданы относительно верхнего левого угла клиентской области окна:
void CWndGeom: lOnMouseMove (UINT nFlags, CPoint point)
{
//====== Если указатель мыши в пределах окна,
if (m_Rect.Pt!nRect (point))
{
//====== то захватываем мышь, выбираем перо
//====== и рисуем обрамляющий прямоугольник
SetCapture () ;
CClientDC dc(this) ;
CPen pen (PS_SOLID, 4, RGB (192, 192, 255));
dc.SelectObject (&pen);
dc.MoveTo(m_Rect.left+4, m_Rect . top+4) ;
dc.LineTo (m_Rect.right-4, m_Rect . top+4) ;
dc.LineTo (m_Rect.right-4, m_Rect .bottom-4) ;
dc.LineTo (m_Rect.left+4, m_Rect .bottom-4) ;
dc.LineTo (m_Rect.left+4 , m_Rect . top+4) ;
}
else
{
ReleaseCapture () ;
// Освобождаем мышь Invalidated;
// Прямоугольник будет стерт
}
}
Так как в коде функции OnLButtonDown содержится обращение к объекту класса CDrawView, то необходимо в список директив препроцессора текущего файла (RightView.cpp) вставить еще одну: #include "DrawView.h".
Отслеживание состояния команд
CDrawView::OnUpdateEditNewpoly(CCradUI *pCmdUI){
pCmdUI->SetCheck(m_bNewPoints);
}
Метод SetCheck вспомогательного класса ccmdui устанавливает флажок рядом с командой меню, если параметр имеет значение TRUE, или снимает его, если параметр имеет значение FALSE. Состояние кнопки на инструментальной панели синхронизировано с состоянием команды меню, имеющей тот же идентификатор.
Следующим шагом в развитии приложения будет введение в действие второй панели инструментов IDR_Draw_TYPE. Загрузка из ресурсов панели инструментов осуществляется методом LoadToolBar класса CToolBar. Так как объект этого класса (m_wndToolBar) хранится в классе главного окна (CMainFrame), то и смену панелей инструментов целесообразно выполнять в этом же классе. Введите в него новый метод:
void CMainFrame::ChangeToolbar(UINT tb)
{
//=== в параметре tb будет передан идентификатор панели
m_wndToolBar.LoadToolBar (tb) ;
//=== Перерисовка toolbar
RecalcLayout();
}
Метод CFrameWnd::RecalcLayout занимается перерисовкой панели инструментов и пересчетом размеров клиентской области окна, так как панель инструментов хоть и управляется классом главного окна, но расположена в клиентской области окна, отнимая у нее часть полезной площади.
Имея в классе главного окна такую функцию, как ChangeToolbar, мы просто должны вызывать ее в нужные моменты, подавая на вход идентификатор той или иной панели. Осталось правильно определить те моменты, когда надо производить смену панелей. Очевидно, это моменты перевода фокуса из окна типа CTreeFrame в окно типа CDrawFrame и наоборот.
Заметьте, что фокус может быть переведен в окно четырьмя разными способами:
Во всех четырех случаях окну-рамке будет послано сообщение WM_SETFOCUS, что нам и надо. Создайте известным вам способом обработчики рассматриваемого сообщения в двух классах окон-рамок CTreeFrame и CDrawFrame и наполните заготовки кодами, как показано ниже:
void CTreeFrame::OnSetFocus(CWnd* pOldWnd)
//====== Родитель делает свое дело,
CMDIChildWnd::OnSetFocus(pOldWnd);
//====== а мы делаем свое
((CMainFrame*)GetParentFrame())
->ChangeToolbar(IDRJTreeTYPE);
void CDrawFrame::OnSetFocus(CWnd* pOldWnd)
CMDIChildWnd::OnSetFocus(pOldWnd);
((CMainFrame*)AfxGetMainWnd())
->ChangeToolbar(IDR_DrawTYPE);
Функция GetParentFrame, полученная в наследство от класса CWnd, прбдвигаясь снизувверх, ищет среди родительских окон ближайшее окно-рамку. В нашем случае в этой цепи будет одно промежуточное окно типа MDICLIENT, управляемое классом cwnd. Отметим, что тип MDICLIENT не документирован, но известно, что он служит для управления окнами-рамками типа CMDlchildWnd, располагающимися в клиентской области главного окна приложения. Наши классы CTreeFrame и CDrawVrame являются потомками CMDlchildWnd, поэтому ими-то и управляет секретное окно типа MDICLIENT. Существует и другой способ получить адрес главного окна (CMainFrame). Это вызов глобальной функции MFC Af xGetMainWnd. Мы используем его во второй версии OnSetFocus только для того, чтобы продемонстрировать оба способа.
Если вы запустите приложение в этот момент, то получите сообщение об ошибках, к которым пора привыкнуть, так как они встречаются довольно часто и вызваны тривиальной причиной — отсутствием видимости класса. Вставьте строку #include "MainFrm.h" в оба файла реализации окон-рамок. Затем запустите приложение вновь и, выбрав шаблон Tree, дайте команду View > Geometry. Вместе с окном другого типа вы увидите и другую панель инструментов. Дайте команду Window > Tile Vertically и проверьте все способы поочередной активизации окон. Панель инструментов и меню должны мгновенно отслеживать переход фокуса.
При записи нового документа в текущую папку или удалении файла из текущей папки ситуация, которую призван отражать класс CRightView, меняется. Для синхронизации вида с изменившейся ситуацией была введена в меню IDR_TreeTYPE команда View > Refresh. Если мы хотим создать обработчик этой команды, то надо решить, в каком классе это лучше всего сделать. Тут есть проблема, которая может быть сначала и не видна. Вы помните, что мы поместили команду Refresh только в одно меню !DR__TreeTYPE. Поэтому она будет доступна только тогда, когда активно окно CTreeFrame, что соответствует логике изменения содержимого правого окна. Мы исходим из того, что изменяемое окно должно быть видно пользователю.
Если создать обработчик только в одном из классов, то команда будет не всегда доступна. Ее доступность зависит от того, в каком из окон находится фокус. Например, пусть обработчик находится в классе CLef tview. Если щелкнуть мышью правое окно, то команда будет недоступна. Она станет вновь доступной, если щелкнуть мышью левое окно. Рассмотрите самостоятельно варианты размещения обработчика В классах CTreeFrame, CMainFrame, (CDrawFrame?). Наряду с доступностью обсудите, как добывать адреса нужных объектов.
Мы решили поместить обработчик в класс документа, так как при этом команда будет относиться к окну CRightView активного документа, что логично. Известным вам способом создайте заготовку функции обработки команды ID_VIEW_ REFRESH и приведите ее в соответствие со следующим фрагментом:
void CTreeDoc::OnViewRefresh(void)
{
//====== Получаем адрес левого представления
CLeftView *pView = dynamic_cast
(GetView(RUNTIME_CLASS(CLeftView)));
//====== Запускаем цепочку действий для освежения
//====== содержимого правого окна
FreeDocs();
pView->SearchForDocs
(pView->GetPath(pView->m_Tree.GetSelectedItem()));
ProcessDocs();
}
Запустив приложение, вы опять получите сообщения об ошибках, и причины будут теми же. Вставьте в TreeDoc.cpp строку #include "Lef tview.h", а в Lef tview.h уберите упреждающее объявление класса CTreeDoc, но вставьте внутрь объявления класса CLef tview декларацию односторонней дружбы:
friend class CTreeDoc;
Теперь запуск должен пройти гладко. Проверьте работу команды View > Refresh, предварительно сохранив документ Save as в ту же папку, которая выбрана в левом окне.
Развитие класса документа
Теперь, когда мы имеем вспомогательные классы (CDPoint и CPolygon), можно подумать о структуре данных класса CTreeDoc. Нам понадобятся:Кроме этого, нам понадобятся методы для управления тремя окнами: CLef tview, CRightView и CDrawView. Последний класс будет управлять окном, в котором полигон может быть отредактирован. Этот класс надо еще создать. Замените существующий интерфейс класса CTreeDoc на тот, который приведен ниже. Здесь мы также провели упрощение начальной заготовки по схеме, обсуждавшейся выше:
class CTreeDoc : public CDocument {
//==== Все 3 представления имеют право доступа
//==== к данным документа
friend class CLeftView;
friend class CRightView;
friend class CDrawView;
protected:
virtual ~CTreeDoc ();
CTreeDoc () ;
DECLARE_DYNCREATE(CTreeDoc) public:
//========== Данные документа============
//
CPolygon m_Poly; // Дежурный полигон VECPOLY m_Shapes;
// Контейнер полигонов
// ====== Контейнер имен файлов
vector
//====== Размер документа в Page space
CSize m_szDoc;
//== Коэффициент увеличения при переходе World->Page
OINT m_nLogZoom;
//====== Флаг: открыто окно типа CTreeFrame
bool m_bTreeExist;
//=====Флаг: открыто окно типа CDrawFrame
bool m_bDrawExist;
//====== Новые методы класса документ =====//
//====== Поиск нужного представления
CView* GetViewfconst CRuntimeClass* pClass);
//====== Создание нужного представления
bool MakeViewO ;
//====== Преобразование координат World -> Page
CPoint MapToLogPt(CDPointS pt);
//====== Преобразование координат Page -> World
CDPoint MapToWorldPt(CPolntS pt) ;
//===== Перерисовка окна редактирования
void UpdateDrawView();
// Чтение найденных документов и их демонстрация
void ProcessDocs();
//====== Освобождение контейнеров
void FreeDocs();
//====== Поиск выбранной точки
int FindPoint(CDPointS pt) ;
// Overrides
public:
virtual BOOL OnNewDocument();
virtual void Serialize(CArchiveS ar) ;
// Generated message map functions
protected:
DECLARE_MESSAGE_MAP()
);
Некоторым из данных документа можно присвоить значения по умолчанию. Обычно это делается в конструкторе класса. Зададимся неким произвольным размером (2000 х 2000) документа в логической (Page) системе координат. Чем больше эта область, тем точнее будут отражены детали конструкции, так как вещественные (World) координаты претерпят округление при приведении к целым (Page) координатам. Вспоминая, что две из наших тестовых фигур имеют габариты в 2 единицы в пространстве World, определяем коэффициент увеличения m_nLogZoom = 700. В этом случае габариты фигур в пространстве Page будут равны 1400 единиц, то есть они целиком поместятся в области документа. Выбрав произвольные начальные цвета фигуры и учтя соображения относительно установки обратного указателя, получим следующую версию конструктора класса CTreeDoc:
CTreeDoc::CTreeDoc() : m_szDoc(2000,2000), m_Poly()
{
//====== Установка обратного указателя и
//====== атрибутов дежурного полигона
m_Poly.Set(this, RGB(240,255,250), RGB(0,96,0), 2);
m_nLogZoom = 700;
}
Деструктор класса должен освобождать память, занимаемую динамическими структурами, входящими в состав класса. Метод FreeDocs мы создадим позже, поэтому при проверочных компиляциях проекта либо создайте заглушку — пустое тело функции FreeDocs, либо временно вставляйте символы комментария в строке с вызовом отсутствующей функции:
CTreeDoc::~CTreeDoc()
{
FreeDocs () ;
m_Poly .m_Points . clear () ;
}
Устойчивость данных документа обеспечивается функцией Serialize, и в стартовой заготовке класса уже есть этот метод. Его тело содержит схему сериализа-ции, но не имеет конкретных кодов записи или чтения данных из архива. Мы должны наполнить заготовку кодами так, чтобы документ мог полностью сохранить или восстановить свое состояние. Нет необходимости сохранять абсолютно все данные документа, так как некоторые из них носят тактический (временный) характер. Они заданы по умолчанию и не будут изменяться, например m_szDoc или m_nLogZoom. С долговременными данными документа мы отождествляем текущий или дежурный полигон m_Poly, который по легенде отражает выбранную и редактируемую в данный момент конструкцию. Он должен полностью изменить свои данные при выборе пользователем одной из картинок в окне правого представления. С этим окном мы связываем контейнер полигонов m_Shapes, который тоже носит временный характер, так как уничтожается и вновь создается при переходе из одной папки в другую и лишь помогает пользователю осуществить выбор. Таким образом, сериализацию документа мы отождествляем с сериали-зацией дежурного полигона. Поэтому тело функции Serialize выглядит весьма просто:
void CTreeDoc: : Serialize (CArchivei ar) {
// Просим объект выполнить сериализацию самостоятельно
m_Poly. Serialize (ar) ;
if (ar.IsStoringO ) {
// Здесь помещается код записи "обычных" данных }
else {
// Здесь помещается код чтения "обычных" данных
Мы могли бы поместить в ветви условного оператора такой код: ar « m_szDoc « m_nLogZoom; ar » m_szDoc » m_nLogZoom; но тогда для обработки документов, расположенных в текущей папке, было бы необходимо поддерживать динамический контейнер объектов CTreeDoc. Чтение документов сводилось бы к вызову Serialize для каждого из них. Такое решение будет более громоздким, чем поддержка контейнера полигонов. Поэтому мы оставляем ветви условного оператора пустыми.
Продолжая развитие темы преобразования координат, создадим тело функции MapToLogPt, которая, получив на входе точку с вещественными World-координатами, возвращает точку с целыми координатами в пространстве Page. В коде этой функции мы помещаем центр симметрии фигуры (точку с координатами
CDPoint(0,0)) в центр логической области, отведенной для документа, увеличиваем координаты и преобразуем их к целому типу:
CPoint CTreeDoc::MapToLogPt(CDPointS pt) {
{
//====== Растяжение и сдвиг
int x = m_szDoc.cx/2 +
int(m_nLogZoom * pt.x), у = m_szDoc.cy/2 +
int(m_nLogZoom * pt.y);
return CPoint(x,y);
+}
Введите также функцию обратного преобразования координат, которая, получив на входе точку с целыми координатами в пространстве Page, вычисляет соответствующую ей точку с вещественными координатами в пространстве World:
CDPoint CTreeDoc::MapToWorldPt(CPointS pt)
{
//====== Обратные операции
double x = double(pt.x - m_szDoc.cx/2) / m_nLogZoom,
у = double(pt.y - m_szDoc.cy/2) / m_nLogZoom;
return CDPoint(x, y);
}
В настоящий момент, если закомментировать вызовы FreeDocs и ProcessDocs в теле деструктора и функции OnSelchanged класса CLef tview, то вы можете запустить приложение, с тем чтобы устранить возможные ошибки. Но пока никакой новой функциональности оно не обнаружит, так как мы еще не занимались созданием и управлением других представлений его документа. Нам вновь придется вернуться к классу документ, но только после того, как будут разработаны классы связанных с ним представлений.
Реакция на уведомляющие сообщения CTreeCtrl
Когда пользователь раскрывает узел дерева, то встроенный в класс CTreeView объект класса CTreeCtrl посылает родительскому окну (нашему представлению CLef tview) уведомляющее сообщение. Оно работает по схеме WM_NOTIFY, которую мы уже рассматривали. Наш класс CLef tview должен реагировать на это сообщение, сканировать раскрываемую папку или логический диск и изменять дерево, вставляя в раскрываемую ветвь новые объекты файловой системы, которые обнаружены внутри папки или диска. Для того чтобы ввести в класс способность реагировать на рассматриваемое сообщение, вы должны:Буква N в имени сообщения говорит о том, что сообщение является уведомляющим, а знак равенства перед ним означает, что оно принадлежит к особой группе отражаемых (reflected) сообщений. В версиях MFC (до 4.0), не было обработчиков отражаемых сообщений. Теперь к старому механизму обработки уведомляющих сообщений от дочерних {child) элементов добавился новый, который позволяет произвести обработку уведомляющего сообщения в классе самого элемента. Уведомляющее сообщение как бы отражается {reflects) назад в класс дочернего окна (элемента управления CTreeCtrl). Этот сценарий мог бы быть реализован и в классе, производном от CTreeCtrl, но нам нет смысла создавать такой класс, так как возможности класса CLef tview вполне достаточны для обработки обоих сообщений. Здесь важен лишь тот факт, что можно перехватить управление в те моменты, когда пользователь манипулирует с деревом.
Первое сообщение (=TVN_ITEMEXPANDING) поступает в момент нажатия маркера (+). Дерево в этот момент еще не раскрылось. Здесь мы должны притормозить процесс перерисовки дерева до того момента, пока не получена вся информация о содержимом раскрываемого узла.
Саму информацию мы будем добывать в теле функции, обрабатывающей второе сообщение (=TVN_ITEMEXPANDED). Оно приходит после того, как узел дерева раскрылся (но не обязательно перерисовался). Здесь мы должны реализовать два варианта развития событий: узел открывается впервые и узел открывается повторно.
Третье сообщение (=TVN_SELCHANGED) приходит в момент, когда пользователь нажал кнопку в пределах самого узла, то есть он выбрал (select) узел. Начнем с обработки первого сообщения. Измените тело функции Onltemexpanding так, чтобы оно имело вид:
void CLeftView::0nltemexpanding (NMHDR* pNMHDR, LRESULT* pResult)
{
//====== Преобразование типа указателя
NM_TREEVIEW* p = (NM_TREEVIEW*)pNMHDR;
//====== Если узел не раскрыт
if ( !(p->itemNew.state & TVIS_EXPANDED))
//====== тормозим перерисовку
SetRedraw(FALSE); *pResult = 0;
}
Бит состояния TVIS_EXPANDED не равен нулю, когда узел уже раскрыт. Мы хотим выделить обратный случай, поэтому пользуемся операцией логического отрицания. Метод cwnd:: SetRedraw позволяет установить флаг перерисовки. Если он снят, то система не будет перерисовывать содержимое окна. Вставьте изменения В тело функции обработки Onltemexpanded:
void CLeftView::OnItemexpanded (NMHDR* pNMHDR, LRESULT* pResult) {
NMJTREEVIEW* p = (NMJTREEVIEW*)pNMHDR;
//====== Создаем курсор ожидания
CWaitCursor wait;
//====== Признак раскрытия узла (а не его закрытия)
if (p->itemNew.state & TVIS_EXPANDED)
{
// Описатели раскрываемого и 1-го вложенного узла
HTREEITEM hCur = p->itemNew.hltem,
h = m_Tree.GetChildItem(hCur);
//====== Если имя вложенного узла пусто,
//====== то ветвь еще не раскрывалась
if (m_Tree.GetItemText(h) == "")
{
//====== Удаляем муляж
m_Tree.DeleteItem(h);
//====== Вычисляем полный путь
CString s = GetPath(hCur) + "*.*";
//====== Наполнение раскрытой ветви
CFileFind cff; BOOL bFound = cff.FindFile(s);
while (bFound) {
bFound = cff.FindNextFile();
if (cff.IsDirectory() && !cff.IsDots())
AddItem(hCur, cff.GetFilePath ()); } }
//====== Разрешаем перерисовку
SetRedraw(TRUE); }
*pResult = 0;
}
Здесь реализованы два варианта развития событий: узел открывается впервые и узел отрывается повторно. Признаком первого варианта является наличие пустого элемента с нулевым индексом изображения и пустой строкой текста внутри раскрываемой ветви. Мы удаляем такой элемент, определяем полный файловый путь раскрываемого узла (папки или диска), сканируем файловый адрес и наполняем дерево новыми элементами. Алгоритм заполнения содержимого папки сходен с алгоритмом заполнения логического диска. Также воспользуемся пустым узлом для пометки папок, которые имеет смысл раскрывать, так как в них есть вложенные папки или файлы. Функция GetPath должна пройти вверх по иерархической структуре дерева и вычислить полный файловый путь узла, заданного параметром. Введите коды этой функции в файл LeftView.cpp:
CString CLeftView::GetPath (HTREEITEM hCur)
{
//====== Вычисляет полный файловый путь узла hCur
CString s = "";
for (HTREEITEM h=hCur; h; h=m_Tree.GetParentItem(h))
s = m_Tree.GetItemText(h) + '\\' + s;
return s; }
Размеры левого окна были заданы в момент создания стартовой заготовки и они, пожалуй, маловаты. Исправьте начальные размеры окна, которые задаются при вызове CreateView внутри функции CTreeFrame::OnCreateClient. Посмотрите справку по этой функции и задайте горизонтальный размер окна равным 200.
Запустите приложение и протестируйте работу дерева. Теперь его поведение должно соответствовать тем требованиям, которые были сформулированы в начале разработки проекта. В такие моменты полезно провести эксперименты, чтобы лучше уяснить смысл некоторых действий. Например, временно уберите битовый флаг SHGFI_SMALLICON при вызове SHGetFileinf о и посмотрите, как изменится вид узлов дерева. Затем временно исключите вызов функции SetRedraw в обработчике Onitemexpanding и пронаблюдайте поведение дерева при раскрытии папки, содержащей большое количество вложенных объектов, например winNT.
Реакция на выбор узла дерева Поиск
CLeftView::OnSelchanged (NMHDR *pNMHDR, LRESULT *pResult){
NM_TREEVIEW* p = (NM_TREEVIEW*)pNMHDR;
//====== Освобождение контейнера текущих файлов
GetDocument()->FreeDocs();
//====== Поиск нужных файлов
SearchForDocs (GetPath(p->itemNew.hItem));
//====== Генерация картинок и демонстрация их в окне
//====== правого представления
GetDocument()->ProcessDocs();
*pResult = 0;
}
Схема обработки сообщения =TVN_SELCHANGED такая же — WM_NOTIFY, но алгоритм отличается. Акцент в обработке переносится в класс документа. Там следует хранить данные о файлах документов, обнаруженных в выбранной папке или на логическом диске, туда же следует ввести новые методы: FreeDocs и ProcessDocs. При изменении выбора пользователя мы:
Поиск документов своего типа (mgn-файлов) производится по той же схеме с использованием класса CFindFile и его методов поиска объектов файловой системы. Но если ранее мы просматривали все объекты, задав маску поиска "*.*", то теперь мы можем сузить поиск, задав маску "* .mgn". Полные пути найденных файлов будем хранить в контейнере m_sFiles типа vector
void CLeftView::SearchForDocs (CString s) {
//====== Дополняем файловый путь маской поиска
s += "*.mgn";
CFileFind cff;
BOOL bFound = cff.FindFile(s);
while (bFound)
{
bFound = cff .FindNextFile() ;
//==== Запоминаем файловые пути в контейнере строк
GetDocument()->m sFiles.push back(cff.GetFilePath());
}
}
Ресурсы шаблона документов Если
CToolBar m_wndToolBar;CStatusBar m_wndStatusBar;
Создание и связывание переменных
CPolyDlg::DoDataExchange(CDataExchange* pDX){
//====== Связывание Control-переменных с ползунками
DDX_Control(pDX, IDC_BSLIDER, m_bSlider);
DDX_Control(pDX, IDCJ3SLIDER, m_gSlider);
DDX_Control(pDX, IDC_RSLIDER, m_rSlider);
//==== Связывание Control-переменных с нестандартными
//==== окнами редактирования
DDX_Control(pDX, IDC_COLOR, m_cColor) ;
DDX_Control(pDX, IDC_BLUE, m_cBlue);
DDX_Control(pDX, IDC_GREEN, m_cGreen);
DDX_Control (pDX, IDC_RED, m_cRed) ;
//==== Связывание Value-переменных с нестандартными
//==== окнами редактирования и проверка данных
DDX_Text(pDX, IDC_BLUE, m_nBlue);
DDV_MinMaxUInt(pDX, m_nBlue, 0, 255);
DDX_Text (pDX, IDC_GREEN, m_nGreen);
DDV_MinMaxUInt(pDX, m_nGreen, 0, 255);
DDX_Text(pDX, IDC_RED, m_nRed) ;
DDV_MinMaxUInt(pDX, m_nRed, 0, 255);
DDX_Text(pDX, IDC_PEN, m_nPen);
DDV_MinMaxUInt(pDX, m_nPen, 1, 100);
//==== Вызов родительской версии функции обмена CDialog::DoDataExchange(pDX);
}
Список изображений, ассоциируемый с деревом
Дерево выглядит значительно лучше, если с каждой его ветвью связать растровое изображение (bitmap image). Обычно с деревом ассоциируется список изображений, управляемый объектом класса cimageList. В общем случае с каждым узлом дерева можно связать два изображения. Одно — для узла в нормальном состоянии, другое — в выбранном. Мы уже ввели в состав класса переменную m_plmgList типа cimageList*, которая должна указывать на сформированный список. Немного позже мы попросим систему дать нам Windows-описатель (HIMAGELIST) поддерживаемого ею списка изображений для дисков, папок и файлов. Однако программист должен уметь самостоятельно формировать список произвольных растровых изображений и связывать его с объектом класса CTreeCtrl. Покажем, как это делается. Создайте несколько bitmap-изображений и присвойте им идентификаторы IDB_IDB_2 и т. д. Последнему изображению присвойте имя IDB_N. Для этого:Вернемся К функции OnlnitialUpdate. После строки m_Tree.SetlmageList... вставьте фрагмент, который задает форму дерева из трех узлов (или ветвей):
//====== Вставляем узел верхнего уровня иерархии
gtv.hParent = TVI_ROOT;
//====== Вставляем в конец списка
gtv.hlnsertAfter = TVI_LAST;
//====== Формат узла — два изображения и текст
gtv.item.mask = TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIFJTEXT;
//=== Индекс изображения для узла в обычном состоянии
gtv.item.iImage = 0;
//=== Индекс изображения для узла в выбранном состоянии
gtv.item.iSelectedlmage = 1;
//====== Текст, именующий узел
gtv.item.pszText = "First";
11====== Описатели трех ветвей
HTREEITEM hi, h2, h3;
//====== Вставка первого узла
hi = m_Tree.Insertltem(Sgtv);
//====== Первый узел будет родителем второго
gtv.hParent = h1;
//====== Атрибуты второго узла
gtv.item.iImage = 1;
gtv.item.pszText = "Second";
//====== Вставка второго узла
h2 = m_Tree.Insertltem(Sgtv);
//====== Второй, узел будет родителем третьего
gtv.hParent = h2;
gtv.item.ilmage = 2;
gtv.item.pszText = "Third";
//====== Вставка третьего узла
h3 = m_Tree.Insertltem(Sgtv);
Запустите приложение, и если вы не забыли создать bitmap-изображения, то они должны появиться слева от текстового ярлыка узла (рис. 5.1). Проанализируйте вложенность узлов дерева. Теперь замените в строке gtv.hParent = b2; b2 на b1и проверьте результат. Затем рекомендуем заменить b1 на константу TVI_ROOT и вновь посмотреть, что получится. Обратите внимание на то, что изображения изменяются при выборе узлов, то есть при переводе курсора мыши с одного узла на другой.

Рис. 5.1. Вид главного окна приложения Tree
Тестирование Приложения, даже
CWndGeom:void CWndGeom::OnMouseMove(UINT nFlags, CPoint point)
{
//====== Два прямоугольника (CWndGeom и CRightView)
CRect rChild, rParent;
//=== Определяем экранные координаты (не клиентские!)
GetWindowRect(rChild) ;
m_pView->GetWindowRect (rParent) ;
//=== Если есть полосы прокрутки, то уменьшаем
//=== прямоугольник окна на толщину полос
if (m_pView->m_szScroll.cx - m_j>View->m_szView.cx > 0)
rParent . right -= SM_CXHSCROLL;
if (m_pView->m_szScroll.cy - m_pView->m_szView.cy > 0)
rParent. bottom -= SM_CYVSCROLL ;
//=== Ищем пересечение прямоугольников, обрезая rChild
rChild.IntersectRect (rChild, rParent);
//=== Приводим к экранным координаты указателя мыши
ClientToScreen (Spoint) ;
//=== Если мышь попала в усеченный прямоугольник,
if ( rChild. PtlnRect (point))
{
//=== то демонстрируем активное состояние,
// изображая рамку внутри прямоугольника CWndGeom
if (GetCaptureO != this)
{
SetCapture() ;
//=== Координаты относительные (клиентские)
CRect r (mJRect) ;
r.DeflateRect (4, 4);
CClientDC do (this) ;
//====== Обрамляем выбранный рисунок
dc.FrameRect (Sr, SCBrush (RGB (192, 192, 255) ) ) ;
}
else
{
//=== Это происходит один раз при выходе из окна
ReleaseCapture () ;
Invalidate () ;
}
}
Здесь я решил применить другой способ обрамления — с помощью функции FrameRect. Она хороша тем, что не закрашивает внутренность прямоугольника, но обладает тем недостатком, что рамка не может иметь толщину более одной логической единицы. Приведем еще один вариант обрамления, использующий толстое перо и прозрачную кисть. Здесь приведен только тот фрагмент, в котором произошли изменения:
if (rChild. PtlnRect (point) )
{
if (GetCaptureO != this)
{
SetCapture () ;
CPen pen (PS_SOLID, 4, RGB (192, 192, 255) );
CClientDC dc(this) ;
dc. SelectObject (&pen) ;
CRect r (m_Rect) ;
//====== Уменьшаем прямоугольник
r .DeflateRect (4,4) ;
//=== Выбираем прозрачную кисть для того, чтобы
//=== не закрасить его содержимое
dc. SelectObject (GetStockObject (NULL_BRUSH) ) ;
dc. Rectangle (r) ;
}
}
На рис. 5.4 приведен вид приложения в момент, когда курсор мыши расположен над окном, отображающим данные неактивного документа. Рамка и курсор обозначают состояние готовности к произведению выбора.



Рис. 5.4. Три представления одного документа
Управление файловым деревом
В этом уроке мы подробно рассмотрим процесс разработки MDI-приложения, в котором один тип документов взаимодействует с несколькими своими представлениями. В рамках архитектуры «документ — представление» принято использовать следующие термины:
Главным моментом в архитектуре является то, что один документ может иметь несколько связанных с ним представлений, но каждое из них может быть связано лишь с одним документом.
Особенностью разрабатываемого приложения является то, что в одном из представлений, управляемых классом cscrollview, пользователь сможет просматривать в качестве «картинок» — чертежей или схем, выбирать и открывать документы своего приложения, которые расположены в файлах с различными адресами. Навигацию по файловому дереву будем осуществлять с помощью второго представления, которым управляет класс CTreeView. Классы CScrollView и CTreeView являются специализированными потомками класса cview. Класс CTreeView тесно связан с классом CTreeCtrl, который разработан как элемент управления произвольным деревом. Мы должны научиться им управлять.
Документ, выбранный пользователем с помощью двух предыдущих представлений, отображается в третьем, производном от cview, которое служит посредником между пользователем и данными документа. В его окне пользователь сможет редактировать данные документа. В качестве данных мы используем динамический массив (контейнер) точек с вещественными координатами, который удачно моделирует произвольный чертеж — двухмерную проекцию какого-либо элемента конструкции. Идеи, заложенные в этом учебном приложении, использованы в реальном проекте по расчету физических полей, описываемых дифференциальными уравнениями в частных производных. В частности, производились расчеты поля магнитов, отсюда проистекает выбранное нами расширение (mgn) для документов приложения. В задачах такого рода исходными являются данные о геометрии расчетной области. Именно она наиболее точно определяет документ (вариант расчета). Если число таких геометрий велико, то поиск варианта по картинке расчетной области существенно упрощает жизнь исследователя физических полей. В связи с этим был получен заказ — ввести в проект возможность поиска и выбора документа по миниатюрному графическому представлению (схеме) геометрии расчетной области. Упрощенная реализация этой части проекта рассмотрена ниже. Начнем с создания стартовой заготовки MDI-приложения.
Визуальное редактирование данных
CDrawView::OnLButtonDown(UINT nFlags, CPoint point){
//====== В режиме создания нового полигона
if (m_bNewPoints)
{
CTreeDoc *pDoc = GetDocument();
//====== Ссылка на массив точек текущего полигона
VECPTSS pts = pDoc->m_Poly.m_Points;
//=== Получаем адрес текущего контекста устройства
CDC *pDC = GetDC() ;
//====== Настраиваем его с учетом размеров окна
SetDC(pDC) ;
//=== Преобразуем аппаратные координаты в логические
pDC->DPtoLP(ipoint);
//=== Преобразуем Page-координаты в World-координаты
CDPoint pt = pDoc->MapToWorldPt(point);
//====== Запоминаем в контейнере
pts.push_back (pt);
}
//====== В режиме готовности к захвату
else if (m_bReady)
{
ra_bLock = true; // Запоминаем состояние захвата
m_bReady = false; // Снимаем флаг готовности
}
//====== В режиме повторного нажатия
else if (mJbLock)
m_bLock = false; // Снимаем флаг захвата
else
//В случае бездумного нажатия
return; // уходим
Invalidated; // Просим перерисовать
}
void CDrawView::OnMouseMove(UINT nFlags, CPoint point)
{
//=== В режиме создания нового полигона не участвуем
if (m_bNewPoints) return;
//====== Получаем и настраиваем контекст
CDC *pDC = GetDCO ;
SetDC(pDC);
//=== Преобразуем аппаратные координаты в логические
pDC->DPtoLP(Spoint);
//=== Преобразуем Page-координаты в World-координаты
CTreeDoc *pDoc = GetDocument();
CDPoint pt = pDoc->MapToWorldPt(point);
//====== Если был захват, то перерисовываем
//====== контуры двух соседних с узлом линий
if (m_bLock)
{
// Курсор должен показывать операцию перемещения
SetCursor(m_hGrab);
//====== Установка режима
pDC->SetROP2(R2_XORPEN);
//====== Двойное рисование
//====== Сначала стираем старые линии
RedrawLines(pDC, pDoc->MapToLogPt (pDoc->
m_Poly.m_Points[ra_CurID]));
//====== Затем рисуем новые
RedrawLines(pDC, point);
//====== Запоминаем новое положение вершины
pDoc->m_Poly.m_Points[m_CurID] = pt;
}
//====== Обычный режим поиска близости к вершине
else
{
m_CurID = pDoc->FindPoint(pt);
// Если близко, то m_CurID получит индекс вершины
// Если далеко, то индекс будет равен -1
m_bReady = m_CurID >= 0;
//=== Если близко, то меняем курсор
if (m_bReady)
SetCursor(m_hGrab);
}
}
//====== Перерисовка двух линий, соединяющих
//====== перемещаемую вершину с двумя соседними
void CDrawView::RedrawLines (CDC *pDC, CPointS point)
{
CTreeDoc *pDoc = GetDocument();
//====== Ссылка на массив точек текущего полигона
VECPTS& pts = pDoc->m_Poly.m_Points;
UINT size = pts.sizeO;
//====== Если полигон вырожден, уходим
if (size < 2) return;
//====== Индексы соседних вершин
int il = m_CurID == 0 ? size - 1 : m_CurID - 1;
int 12 = m_CurID == size - 1 ? 0 : m_CurID + 1;
// ====== Берем перо и рисуем две линии
pDC->SelectObject(Sm_penLine);
pDC->MoveTo(pDoc->MapToLogPt(pts[11] ) ) ;
pDC->LineTo(point);
pDC->LineTo(pDoc->MapToLogPt(pts[12]));
}
Определение индекса вершины, к которой достаточно близко подобрался указатель мыши, производится в методе FindPoint класса документа. В случае если степень близости недостаточна, функция возвращает значение -1. Вставьте этот метод в файл реализации класса (TreeDoc.cpp):
int CTreeDoc::FindPoint(CDPointS pt)
{
//====== Пессимистический прогноз
int id = -1;
//====== Поиск среди точек дежуоного полигона
for (UINT 1=0; i
{
//=== Степень близости в World-пространстве.
//=== Здесь мы используем операцию взятия нормы
//=== вектора, которую определили в классе CDPoint
if ( !(m_Poly.m_Points[i) - pt) <= 5e-2)
(
id = i;
break; // Нашли
}
}
//====== Возвращаем результат
return id;
}
В этот момент вы можете запустить приложение, выбрать шаблон Draw и проверить возможности визуального редактирования, перетаскивая вершины звезды в пределах клиентской области окна документа.
Включение или выключение второго режима редактирования, служащего для создания нового полигона и ввода координат вершин с помощью мыши, потребует меньше усилий, так как логика самого режима уже реализована в обработчике нажатия левой кнопки мыши. Для включения или выключения (toggle) второго режима используется одна и та же команда. Создайте обработчик команды Edit > New Poly. Для этого:

Рис. 5.3. Редактируемый полигон
В теле обработчика следует установить флаги состояния, уничтожить все вершины дежурного полигона и перерисовать представление:
void CDrawView::OnEditNewpoly(void)
{
//====== Включаем/Выключаем режим ввода вершин
m_bNewPoints = !m_bNewPoints;
//=== Снимаем флаги редактирования перетаскиванием
m_bReady = false;
m_bLock = false;
//====== Если режим включен, то уничтожаем вершины
if (m_bNewPoints)
{
GetDocument()->m_Poly.m_Points.clear() ;
Invalidate();
}
}
Запустите приложение, выберите шаблон Draw и дайте команду Edit > New Poly. Щелкайте левой кнопкой мыши разные места клиентской области окна и наблюдайте за трансформациями полигона m_Poly при добавлении в контейнер его точек новых значений. Мысленно проследите за преобразованиями координат, которые происходят в эти моменты. Вы помните, что мышь дает аппаратные координаты, а в контейнер попадают World-координаты вершин полигона?
Вспомогательные функции Задание
CPolygon::MakeStar(){
m_Points.clear();
//====== Вспомогательные переменные
double pi = 4. * atan(l.), // Углы
al = pi / 10.,
а2 = 3. * al,
//====== 2 характерные точки
xl = cos (al),
yl = sin(al),
x2 = cos(a2),
y2 = sin(a2);
//=== Вещественные (World) координаты углов звезды m_Points.push_back(CDPoint(0., 1.));
m_Points.push_back(CDPoint <-x2, -y2));
m_Points.push_back(CDPoint( xl, yl) ) ;
m_Points.push_back(CDPoint(-xl, yl)) ;
m_Points.push_back(CDPoint( x2, -y2));
//====== Габариты звезды
m_ptLT = CDPoint(-xl, 1.);
m_ptRB = CDPoint( xl,-y2);
//====== Генерация треугольника
void CPolygon::MakeTria() {
m_Points.clear();
double pi = 4. * atand(1.);
a = pi / 6.;
x = cos (a) ;
у = sin(a);
m_Points.push_back (CDPoint(0., 1.));
m_Points,push_back (CDPoint(-x, -y) );
m_Points.push_back (CDPoint( x, -y));
m_ptLT = CDPoint (-x, 1.) ;
m_ptRB = CDPoint ( x,-y);
//====== Генерация пятиугольника
void CPolygon::MakePent()
{
m_Points.clear ();
double pi = 4. * atan(l.),
al = pi / 10.,
a2 - 3. * al,
xl = cos(al),
yl = sin(al),
x2 = cos(a2),
y2 = sin(a2);
// Вещественные (World) координаты углов пятиугольника m_Points.push_back(CDPoint (0 ., 1.));
m_Points.push_back(CDPoint(-xl, yl));
m_Points.push_back(CDPoint(-x2, -y2));
m_Points.push_back(CDPoint( x2, -y2));
m_Points.push_back(CDPoint( xl, yl));
m_ptLT = CDPoint(-xl, 1.);
m_ptRB = CDPoint( xl,-y2);
Взаимодействие представлений документа
"RightView.h"#include "DrawView.h"
Затем перейдем к реализации заявленного в классе документа метода Getview (поиск адреса нужного представления). Его параметром служит адрес статической структуры типа CRuntimeClass, которая присутствует во всех классах, произведенных от cob j ect. Она является общей для всех объектов одного и того же класса и содержит ряд полезных полей, в том числе и поле m_lpszClassName, которое позволяет узнать имя класса на этапе выполнения программы. Обычно для того, чтобы узнать, принадлежит ли объект (адрес структуры CRuntimeClass которого вы знаете) тому или иному классу, пользуются функцией isKindOf, унаследованной от CObject. Она, в свою очередь, для ответа на этот вопрос использует поле m_lpszClassName структуры CRuntimeClass:
CView* CTreeDoc::GetView(const CRuntimeClass* pClass)
{
// Становимся в начало списка представлений документ^
POSITION pos = GetFirstViewPosition();
//====== Пессимистический прогноз
CView *pView = 0;
//====== Цикл поиска нужного представления
while (pos)
{
pView = GetNextView(pos);
//=== Если нашли, то возвращаем адрес
if (pView->IsKindOf(pClass))
break;
}
//===== Возвращаем результат поиска return pView;
}
В процессе работы с MDI-приложением пользователь закрывает одни документы и открывает другие. Вновь открытый документ в начальный момент представлен одним из двух возможных типов окон: либо расщепленным окном типа CTreeFrame, которое содержит два окна CLef tview и CRightview, либо обычным MDI-child-окном типа CDrawFrame, которое содержит одно окно CDrawView. В ситуации, когда пользователь по картинке выбрал в правом окне один из документов, по сценарию необходимо создать новое окно типа CDrawFrame и в его клиентскую область поместить альтернативное представление (CDrawView) выбранного документа. Целесообразно реализовать и обратный сценарий, когда, имея окно типа CDrawView, пользователь хочет создать окно типа CTreeFrame, обрамляющего другие два представления документа.
Создание и инициализация новых окон того или иного типа в MDI-приложени-ях производится с помощью методов класса CDocTemplate, так как именно шаблон документа хранит информацию обо всех членах квартета, ответственных за создание окна документа. Список всех шаблонов документов, поддерживаемых приложением, хранит объект theApp класса СТгееАрр. Класс cwinApp, от которого происходит класс СТгееАрр, предоставляет стандартные методы для работы со списком шаблонов. Метод GetFirstDocTemplatePosition устанавливает позицию (переменную вспомогательного типа POSITION для работы со списками) на первый шаблон списка. Метод GetNextDocTemplate обычным образом возвращает адрес текущего шаблона и после этого сдвигает позицию на следующий элемент списка. Подобный стиль работы со списками поддерживается и другими классами MFC. Привыкнув к нему, вы сэкономите массу усилий в будущем.
Однако в нашем случае, когда существуют только два шаблона документов, нет необходимости искать в списке шаблонов. Мы просто запомнили их адреса (m_pTemplTree, m_pTemplDraw) в объекте theApp класса СТгееАрр. Теперь в любой момент жизни приложения мы можем добыть их и использовать, например для создания новых окон того или иного типа. Ниже приведен метод MakeView класса CTreeDoc, который выполняет указанное действие.
Вы помните, что в любой точке программы мы имеем право вызвать глобальную функцию MFC. Напомним, однако, что почти все глобальные объекты MFC имеют префикс Afx (Application frameworks) — каркас приложения. Среди них есть много действительно полезных функций. Посмотрите справку по индексу Af х, и вы увидите все множество. Традиционно, для того чтобы достать адрес объекта theApp класса приложения, пользуются функцией Af xGetApp. Существует и второй способ — непосредственно использовать глобально определенный объект theApp, но для этого необходимо в начало срр-файла, где предполагается его использовать, поместить строку, разрешающую проблему видимости объекта theApp:
extern СТгееАрр theApp; // Определен в другом месте
В файл реализации класса CTreeDoc вставьте тело функции MakeView, которое приведено ниже. В ней реализован доступ к приложению с помощью глобальной функции AfxGetApp, но вы можете опробовать и второй способ, заменив "рАрр->" на " theApp. " и учтя сказанное выше. При этом также отпадает необходимость в строке кода СТгееАрр* рАрр = (СТгееАрр*) Af xGetApp ();.
bool CTreeDoc::MakeView()
{
//==== Если недостает какого-либо из представлений
if (!m_bDrawExist !m_bTreeExist)
{
//====== Добываем адрес приложения
CTreeApp* pApp = (CTreeApp*) AfxGetApp ();
CDocTemplate *pTempl;
//====== Выбираем шаблон недостающего типа
if ( !m_bDrawExist)
{
pTempl = pApp->m_pTemplDraw;
m_bDrawExist = true;
}
else
{
pTempl = pApp->m_pTemplTree;
m_bTreeExist = true;
// Создаем окно документа
// Тип рамки и представления определяется шаблоном
CFrameWnd *pFrarae = pTempl->CreateNewFrame (this, 0) ; pTempl->InitialUpdateFrame (pFrarae, this) ;
return true;
}
return false;
}
Если вы хотите иметь современный и чуть более надежный код, то используйте вызов:
CTreeApp* pApp = dynamic_cast
Всю работу по созданию окна-рамки и помещения в его клиентскую область выполняют методы CreateNewFrame И InitialUpdateFrame класса CDocTemplate, который является базовым для класса CMultiDocTemplate. Вы помните, что два объекта последнего класса мы создали в теле функции initlnstance для реализации MDI-функциональности по нашему сценарию. Сценарий еще пока не реализован. Введем изменения в метод OnNewDocument, для того чтобы правильно установить флаги существования окон:
BOOL CTreeDoc: : OnNewDocument ()
{
//====== При создании нового документа
if ( ICDocument: : OnNewDocument () )
return FALSE;
//====== Документ знает свой шаблон
CDocTemplate* pTempl = GetDocTemplate () ;
CString s;
//====== Выясняем его тип из строкового ресурса
pTempl->GetDocStrlng (s, CDocTemplate: : fileNewName) ;
m_bDrawExist — s == "Draw";
m_bTreeExist = !m_bDrawExist;
return TRUE;
}
При создании нового документа пользователь выбирает один из двух шаблонов (Tree, Draw), предложенных ему в диалоге New, который, как вы помните, поддерживает каркас приложения. Наша задача — выяснить выбор, сделанный пользователем. Это можно сделать с помощью одного из членов квартета, а именно строкового ресурса, связанного с каждым из шаблонов. Метод GetDocString выделяет подстроку комплексной строки, и по ее содержимому мы узнаем выбор пользователя.
Перейдем к разработке следующего метода класса CTreeDoc. При переводе фокуса с одного узла дерева на другой мы должны освободить память, занимаемую контейнером полигонов m_Shapes и другими временными данными, которые соответствуют документам, обнаруженным в текущей папке. Эти действия выполняет метод FreeDocs. При освобождении контейнера методом clear он вызывает для каждого из своих объектов деструктор. Так.как класс CPolygon мы снабдили деструктором, освобождающим свой вложенный контейнер точек (CDPoint), то вызов m_Shapes. clear (); порождает целую цепочку действий, которую вы можете проследить. Для этого установите точку останова (F9) в теле деструктора класса CPolygon, запустите приложение в режиме отладки (F5) и откройте окно Call Stack, которое позволяет увидеть всю цепочку вызовов функций. Открыть окно Call Stack вы сможете, дав команду Debug > Windows > Call Stack. Команда доступна только в режиме отладки (F5):
void CTreeDoc::FreeDocs()
{
m_sFiles.clear(); m_Shapes.clear();
//====== Выясняем адрес правого окна
CRightView *pView = dynamic_cast
(GetView(RUNTIME_CLASS(CRightView)));
//====== Освобождаем окна-картинки
if (pView) pView->Clear();
}
При обращении к функции Getview мы должны подать на вход адрес структуры CRuntimeClass, которая характеризует искомый класс. Это можно сделать двумя способами: используя макроподстановку RUNTIME_CLASS(), как и сделано выше, или подставив более длинное, но разъясняющее суть макроса, выражение:
Getview(SCRightView::classCRightView)
Выражения:
RUNTIME_CLASS(CRightView)
И
&CRightView::classCRightView
эквивалентны. Вторая форма записи подсказывает вам, что в классе CRightView определена статическая переменная classCRightview типа CRuntimeClass, которая помогает по адресу объекта определить его тип на этапе выполнения.
Рассмотрим метод ProcessDocs класса CTreeDoc, который обрабатывает информацию о файлах документов, обнаруженных в текущей папке. Здесь демонстрируется, как связать архив (объект класса CArchive) с файлом (объектом класса CFile) и заставить объект прочесть данные из файла. Для этой цели используется всего" один временный объект poly класса с Polygon. Данные очередного документа сначала читаются из файла в этот объект — poly. Serialize (ar); а затем весь объект помещается в контейнер — m_Shapes .push_back (poly). Контейнеры устроены таким образом, что они создают свою собственную копию объекта и именно ее и хранят. Благодаря этому мы можем многократно использовать временный объект poly:
void CTreeDoc::ProcessDocs()
{
UINT nFiles = m_sFiles.size();
//====== Если документы не обнаружены
if (!nFiles)
return;
for (UINT i=0; i < nFiles; i++)
{
//====== Читаем все документы
GFile file; // Класс, управляющий файлами
CFileException e; // Класс для обработки сбоев
CString fn = m_sFiles[i); // Имя файла
if (Ifile.Open (fn, CFile::modeRead |
CFile::shareDenyWrite, &e) )
{
//=== В случае сбоя в зависимости от причины
//=== выдаем то или иное сообщение
CString rasg =
e.m_cause == CFileException::fileNotFound ? "Файл: " + fn + " не найден" : "Невозможно открыть " + fn; AfxMessageBox(msg);
return;
}
//====== Связываем архив с файлом
CArchive ar (sfile, CArchive::load);
CPolygon poly; // Временный полигон poly.Set(this);
// Обратный указатель poly.Serialize (ar);
//Читаем данные m_Shapes.push_back(poly);
// Запоминаем в массиве
}
//====== Отображаем результат в правом окне
CRightView *pView - dynamic_cast
(GetView(RUNTIME_CLASS(CRightView)));
pView->Show();
}
При работе с классами CFile, CFileException и CArchive используются статические переменные, которые задают режимы работы. Так, битовые флаги CFile::modeRead (для чтения) и CFile::shareDenyWrite (запретить запись всем другим процессам) задают режим открытия файла. Переменная CArchive::load (чтение) определяет направление сериализации.
Мы сделали достаточно много для правильного взаимодействия представлений документа, но при закрытии какого-либо из окон флаги m_bTreeExist и m_bDrawExist остаются неизменными, что, несомненно, нарушит логику поведения приложения. Событие закрытия окна-рамки необходимо обработать и скорректировать соответствующий флаг. Поэтому введите в классы CTreeFrame и CDrawFrame реакции на сообщение WM_CLOSE и вставьте внутрь обработчиков следующие коды:
void CTreeFrame::OnClose()
{
//====== Добываем адрес активного документа
CTreeDoc *pDoc = dynamic_cast
pDoc->m_bTreeExist = false;
CMDIChildWnd::OnClose();
}
void CDrawFrame::OnClose()
void CDrawFrame::OnClose()
{
CTreeDoc *pDoc = dynamic_cast
pDoc->m_bDrawExist = false;
CMDIChildWnd::OnClose() ;
}
Вы уже, наверное, привыкли к тому, что при введении функций-обработчиков, которые обращаются к объектам других классов приложения, надо корректировать директивы подключения заголовочных файлов. Вот и сейчас надо вставить директиву #include "TreeDoc.h" в файл реализации класса CDrawFrame.
В настоящий момент приложение готово к запуску. Уберите временные комментарии, которые вставляли раньше, запустите приложение, устраните ошибки и протестируйте. Его поведение должно быть ближе к задуманному. Для проверки необходимо с помощью команды File > Save as записать некоторое количество документов, давая им различные имена. После этого следует убедиться, что каждый раз, как фокус выбора попадает в папку, где записаны документы, в правом окне появляются мини-окна типа cwndGeom с изображением полигона. При выборе одного их них щелчком левой кнопки мыши должно создаваться и активизироваться новое окно типа CDrawView. В этот момент полезно дать команду Window > Tile Horizontally, для того чтобы увидеть оба типа окон-рамок со всеми тремя представлениями одного документа. Если документы сохранить на гибком диске (и держать диск в дисководе), то они должны отображаются сразу после запуска приложения, так как сообщение =TVN_SELCHANGED поступает при инициализации левого окна.
Учебник по Visual C++ .Net
Двойная буферизация
В настоящий момент перерисовка изображения во время манипуляций мышью очень плохая, так как мы работаем с одним (front) буфером. Пора подключать второй. Вместо вызова glFlush; вставьте вызов функции auxSwapBuffers();J- из другой библиотеки, которая, как вы помните, не документирована. Но этого мало — надо заменить волшебное слово SINGLE на не менее волшебное слово —DOUBLE. Местоположение вычислите самостоятельно. Поиск места вынуждает прокручивать в голове последовательность вызовов функций, что является полезным, а для многих и необходимым упражнением. После этого запустите приложение и отметьте, что управляемость кубика улучшилась, но при достаточно большом его повороте вокруг оси Y поворот вокруг оси X ведет себя так, как будто сама ось «повернута». Если вы поменяете порядок вызова двух функций вращения glRotated, то эффект останется, но проявит себя в симметричном варианте. Исправьте это, если хотите. Хорошая задача на сообразительность, так как не требует специфических знаний языка программирования, а только общих представлений о сути преобразований и возможностях библиотек OpenGL.
В примерах MSDN можно найти способ введения реакций на нажатия клавиш. Используем клавиши стрелок для смещения объекта в плоскости Z = const. Введите в функцию main декларацию 4 обработчиков:
auxKeyFunc(AUX_DOWN, KeyDown);
auxKeyFunc(AUX_UP, KeyUp);
auxKeyFunc(AUX_LEFT, KeyLeft);
auxKeyFunc(AUX_RIGHT, KeyRight);
Теперь по аналогии с мышиными событиями создайте самостоятельно функции обработки и меняйте внутри них те переменные, от которых зависит трансляция изображения. Например:
void _stdcall KeyDown()
{
gdTransY -=0.1; // Сдвигаем изображение вниз
}
void _stdcall KeyUp()
{
gdTransY += 0.1; // Сдвигаем изображение вверх
}
void _stdcall KeyLeft()
{
gdTransX -=0.1; // Сдвигаем изображение влево
}
void _stdcall KeyRight()
{
gdTransX +=0.1; // Сдвигаем изображение вправо
}
При тестировании результата обратите внимание на поведение изображения. Например, чем больше сдвиг вправо, тем лучше видна левая боковая грань. Кажется, что совместно с перемещением объекта он поворачивается. Но это не так. Эффект объясняется особенностями перспективной проекции.
Графика OpenGL
В этом разделе мы научимся создавать трехмерные изображения с помощью функций библиотеки OpenGL, для того чтобы в следующей главе разработать Windows-приложение, которое можно рассматривать как инструмент просмотра результатов научных расчетов. Материал этого раздела позволит вам постепенно войти в курс дела и овладеть очень привлекательной технологией создания и управления трехмерными изображениями. Сначала мы рассмотрим основные возможности библиотеки OpenGL, затем научимся управлять функциями OpenGL на примере простых приложений консольного типа и лишь после этого приступим к разработке Windows-приложения.
Интерактивное управление положением
OnButtonDown(AUX_EVENTREC *pEvent){
//====== Запоминаем координаты мыши
giX = pEvent->data[AUX_MOUSEX];
giY = pEvent->data[AUX_MOUSEY];
}
static void _stdcall OnLMouseMove(AUX_EVENTREC *pEvent)
{
//====== Узнаем текущие координаты
int x = pEvent->data[AUX_MOUSEX];
int у = pEvent->data[AUX_MOUSEY];
//====== Изменяем углы поворота пропорционально
//====== смещению мыши
gdAngleX += (у - giY)/10.f;
gdAngleY += (x - giX)/10.f;
//====== Запоминаем координаты мыши
giX = x; giY = у; >
Static void _stdcall OnRMouseMove(AUX_EVENTREC *pEvent)
int x = pEvent->data[AUX_MOUSEX];
int у = pEvent->data[AUX_MOUSEY] ;
//=====<= На сколько удалить или приблизить
double dx = (x - giX)/200.f;
double dy = (y - giY)/200.f;
//====== Удаляем или приближаем
gdTransZ += (dx + dy)/2.f;
//====== Запоминаем координаты мыши
giX = x; giY = y;
}
Запустите и опробуйте. Кубик должен управляться, но в обработке мышиных событий присутствует явная ошибка. Для того чтобы ее увидеть, нажмите правую кнопку и выведите курсор мыши за пределы окна влево. Изображение исчезло. один из слушателей наших курсов (Халип В. М. E-mail: viktor@mail.ru) самостоятельно нашел объяснение этому казусу и устранил дефект. Для того чтобы обнаружить его, вставьте в список директив препроцессора еще одну — #include
int x = pEvent->data[AUX_MOUSEXJ;
int у = pEvent->data[AUX_MOUSEY];
на
short x = pEvent->data[AUX_MOUSEX];
short у = pEvent->data[AUX_MOUSEY];
в функциях OnLMouseMove и OnRMouseMove. Теперь повторите опыт и убедитесь в том, что, переходя через границу окна, координата х изменяется монотонно и приобретает отрицательные значения. Чтобы быть последовательным, замените тип глобальных данных для хранения текущей позиции курсора мыши. Вместо int giX, giY; вставьте short giX, giY;. Объяснение эффекта мы оставляем читателю в качестве упражнения по информатике.
Интерполяция цвета Вы можете запустить
(int i = 0; i < 6; i++) ( glNormalSdv (norm[i] ) ;//====== 4 вершины одной грани
for (int j = 0; j < 4; j++)
{
//====== Задаем различные цвета
glColorSd (rand()%10/10.,
rand()%10/10., rand()%10/10.) ;
glVertex3fv(v[id[i] [ j ] ] ) ;
}
}
glEnd() ;
glEndList () ;
Включите в начало файла директиву препроцессора:
#include
для того чтобы стала доступной функция timeQ. Она помогает настроить генератор псевдослучайных чисел так, чтобы при разных запусках программы получать различные комбинации цветов. Двойное деление на 10 (rand()%10/10.) позволяет масштабировать и нормировать компоненты цвета. Запустите и проверьте качество интерполяции цветов.
Использование списков С кубиком
Init (){
glClearColor (1., 1., 1., 0.);
//====== Включаем интерполяцию цветов полигона
glShadeModel (GL_SMOOTH);
glShadeModel (GL_DEPTH_TEST) ;
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL),
glEnable(GL_LIGHTING);
glEnable(GL_LIGHTO);
glEnable(GL_COLOR_MATERIAL);
//====== Готовим сцену
DrawScene () ;
}
Как создать сферу
Для того чтобы из существующей заготовки — икосаэдра из двадцати граней — создать сферу, круглую, блестящую и без изъянов, нужно осуществить предельный переход, как в матанализе, бесконечно увеличивая число треугольников при бесконечном уменьшении их размеров. В дискретном мире нет места предельным переходам, поэтому вместо бесконечного деления надо ограничиться каким-то конечным числом и начать делить каждый из двадцати треугольников икосаэдра на все более мелкие правильные треугольники. Вычисление нормали при этом упрощается, так как при приближении к шару нормаль в каждой вершине треугольника приближается к нормали поверхности шара. А последняя равна нормированному вектору радиуса текущей точки. Алгоритм деления проиллюстрируем рисунком (рис. 6.3).
Рис. 6.3. Деление треугольника икосаэдра
Треугольник с вершинами VI, V2 и V3 разбивается на четыре треугольника: (V1,V12,V31), (V2,V23,V12), (V3,V32,V23) и (V12.V23.V31). После этого промежуточные точки деления надо посадить на поверхность шара, то есть изменить их координаты так, чтобы концы векторов (V12, V23 и V31) дотянулись до поверхности шара. Для этого достаточно нормировать векторы с помощью уже существующей процедуры Scale. Она впоследствии будет использована как для масштабирования нормали, так и для нормировки координат вершин новых треугольников. Но сейчас мы будем вычислять нормаль приближенно. Введем еще две вспомогательные функции:
//=== Команды OpenGL для изображения одного треугольника
void setTria(double *vl, double *v2, double *v3)
{
//====== Нормаль и вершина задаются одним вектором
glNormal3dv(vl);
glVertex3dv(vl);
glNormalSdv (v2);
glVertex3dv(v2);
glNormal3dv(v3);
glVertex3dv(v3);
glEnd() ;
}
//====== Генерация внутренних треугольников
void Split(double *vl, double *v2, double *v3)
{
//====== Промежуточные вершины
double v!2[3], v23[3], v31[3);
for (int l=0; l< 3; i++) {
//====== Можно не делить пополам,
//====== так как будем нормировать
v12[i] = vl[i]+v2[i];
v23[i] = v2[i]+v3[i];
v31 [i] = v3[i]+vl [i];
}
//====== Нормируем три новые вершины
Scale(v!2);
Scale(v23);
Scale(v31); //====== и рисуем четыре треугольника
setTria(vl, v!2, v31);
setTria (v2, v23, v!2);
setTria(v3, v31, v23);
setTria(v!2,v23, v31);
}
Вставьте эти глобальные функции в файл и дайте следующую версию функцию DrawScene, в которой отсутствует вызов функции getNorm для точного вычисления нормали, но есть вызов функции Split для каждой из 20 граней икосаэдра. В результате мы получаем фигуру из 80 треугольных граней, которая значительно ближе к сфере, чем икосаэдр:
void DrawScene()
{
static double
angle = 3. * atan(l.)/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)
};
glNewList(l,GL_COMPILE);
glColor3d (1., 0.4, 1.) ;
glBegin(GLJTRIANGLES);
for (int i = 0; i < 20; i++)
Split (v[id[i][0]], v[id[i][l]], v[id[i] [2] ]) ;
glEnd() ;
glEndList () ;
}
На этой стадии я рекомендую посмотреть, какие интересные и неожиданные результаты могут быть получены вследствие ошибок. Все мы ошибаемся, вот и я так долго возился с направлением обхода и со знаком нормали, что в промежуточных вариантах получал чудовищные комбинации. Многие из них «канули в Лету», но один любопытный вариант легко смоделировать. Если ошибки происходят в условиях симметричного отражения, то возникают ситуации, сходные со случайными изменениями узоров в калейдоскопе. Замените на обратные знаки компонентов вектора в функции Scale. Это действие в предыдущих версиях программы было эквивалентно изменению знака нормали. Найдите строку, похожую на ту, что приведена ниже, и замените знаки так, как показано, на минусы.
v[0] /= -d; v[l] /= -d; v[2] /= -d;
Как убирать внутренние линии Каждой
OnDraw(){
glClear (GL_COLOR_BOFFER_BIT); glColorSd (1., 0.4, 1.);
//====== Вогнутый шестиугольник, но мы зададим его
//====== в виде двух четырехугольников
float с[6][3] =
{
200. 200.,0.,
200. 100.,0.,
250. 20.,0.,
300. 100.,0.,
300. 200.,0.,
250. 100.,0.,
};
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glBegin(GL_QUADS);
glVertex3fv(c[5])
glVertex3fv(c[0])
glVertex3fv(c[l])
glVertex3fv(c[2])
glVertex3fv(c[5])
glVertex3fv(c[2])
glVertex3fv(c[3])
glVertex3fv(c[4])
glEnd();
glFlush ();
}
Массивы вершин, нормалей и цветов
Неэффективность алгоритма последовательного рисования большого числа примитивов не является тайной для тех, кто имеет дело с трехмерной графикой. Поэтому в технологии OpenGL существует ряд приемов (поддержанных функциями), которые основаны на использовании заранее заготовленных массивов, а также списков команд OpenGL. Они значительно повышают эффективность работы конвейера при передаче (rendering) изображений, составленных из десятков и сотен тысяч примитивов. Например, функция glDrawElements позволяет задать геометрические примитивы экономичным способом, то есть с минимальными затратами на вызовы функций. До сих пор мы вызывали в среднем 4-5 функций для каждого треугольника. При этом многократно повторялись, так как вершины, общие для смежных треугольников, задавались не один раз. Массивы величин, ассоциируемых с вершинами (координаты, нормали, цвета и другие), могут быть сформированы заранее и использованы при описании геометрии с помощью массива индексов. Функция glDrawElements требует в качестве одного из параметров массив индексов вершин полигонов. Вот ее прототип:void glDrawElements (GLenum mode, GLsizei count,
GLenum type, const GLvoid *indices);
Функция конструирует count элементов типа mode. Параметр indices должен содержать адрес массива индексов, который формируется заранее. Параметр type определяет тип элементов массива indices. Он может принимать одно из трех фиксированных значений: GL_UNSIGNED_BYTE (используется 8-битовый индекс), GL_UNSIGNED_SHORT (16-биТНЫЙ ИНДСКС), GL_UNSIGNED_INT (32-биТНЫЙ). Характерной особенностью рассматриваемой технологии является то, что величины, ассоциируемые с каждой вершиной примитива, могут храниться в разных массивах или в одном массиве структур с разными полями. Они задаются с помощью 6 функций:
Другой массив индексов — indices, определяет порядок выбора элементов из этих шести массивов. Но этого мало — надо произвести еще некоторые настройки в машине состояний OpenGL. Для перевода ее в режим использования массивов надо несколько раз вызвать функцию glEnableClientstate. Каждый вызов включает один из шести рассмотренных режимов. Только после этого функция glDrawElements способна эффективно задать сразу все примитивы. Например, вызов:
glEnableClientState(GL_VERTEX_ARRAY);
включает режим использования массива координат вершин, а вызов этой же функции с параметром GL_NORMAL_ARRAY включает использование массива нормалей.
Совместно с командой glDrawElements обычно используют тот способ повышения эффективности отображения примитивов, который мы уже используем. Речь идет о паре функций: glNewList, glEndList. Все команды OpenGL, заданные между вызовами этих двух функций, оптимизируются, компилируются (по выбору) и запоминаются в отдельном нумеруемом списке.
Обзор возможностей библиотеки OpenGL
Читатель, наверное, знает, что OpenGL это оптимизированная, высокопроизводительная графическая библиотека функций и типов данных для отображения двух-и трехмерной графики. Стандарт OpenGL был утвержден в 1992 г. Он основан на библиотеке IRIS GL, разработанной компанией Silicon Graphics (www.sgi.com). OpenGL поддерживают все платформы. Кроме того, OpenGL поддержана аппа-ратно. Существуют видеокарты с акселераторами и специализированные SD-кар-ты, которые выполняют примитивы OpenGL на аппаратном уровне.Материал первой части этого урока навеян очень хорошей книгой (доступной в online-варианте) издательства Addison-Wesley «OpenGL Programming Guide, The Official Guide to Learning OpenGL». Если читатель владеет английским языком, то мы рекомендуем ее прочесть.
Ограничения Microsoft К сожалению
Схема конвейера OpenGLСписки команд 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) и данными о текстуре. Такая точка вместе с сопутствующей информацией называется фрагментом. Фрагмент изменяет соответствующий ему пиксел в буфере кадра, если он проходит пять тестов:
Кроме того, фрагмент претерпевает другие изменения.
Основные этапы Для того чтобы
На примере многочисленных хранителей экрана (screen-saver) вы видели, как гладко работает анимация в OpenGL. OpenGL использует два буфера памяти (front and back). Первый (front-буфер) отображается на экране, второй в это время может обрабатываться процессором. Когда обработка закончится, то есть очередная сцена будет готова, вы можете произвести быстрое переключение буферов (swap), обеспечивая тем самым гладкую анимацию изображения. При обмене копирование массивов не происходит, изменяется лишь значение указателя (адреса) отображаемого блока памяти. Отметьте, что процесс рисования в back-буфер происходит быстрее, чем в front, так как большинство видеокарт запрещают редактировать изображение в момент вертикальной развертки, а это происходит 60-90 раз в секунду.Рассмотрим основную схему алгоритма анимации, используемого в OpenGL-при-ложениях. В кино эффект движения достигается тем, что каждый кадр проецируется на экран в течение короткого промежутка времени, затем шторка проектора моментально закрывается, пленка продвигается на один кадр, вновь открывается шторка и цикл повторяется. Период цикла равен 1/24 с или даже 1/48 с в современных кинопроекторах. Современные компьютеры допускают смену кадра (refresh rate) до 120 раз в секунду. Рассмотренный алгоритм можно записать так.
в цикле по всем кадрам:
Если реализовать анимацию по такой схеме, то эффект будет тем более удручающим, чем ближе к 1/24 с подходит время создания изображения, так как полное изображение существует на экране лишь долю периода. Большую часть периода мы видим процесс рисования.
OpenGL предоставляет возможность двойной буферизации (аппаратной или программной — зависит от видеокарты). Алгоритм анимации в этом случае таков: пока проецируется первый кадр, создается второй. Переключение кадров происходит только после того, как закончится формирование второго кадра. Пользователь никогда не видит незавершенный кадр. Эту схему можно представить в виде проектора с двумя кадрами пленки. В момент демонстрации первого второй стирается и вновь рисуется. Новый алгоритм можно записать в цикле по кадрам:
Последний шаг алгоритма не начнет выполняться, пока не закончится предыдущий шаг — создание нового кадра в back-буфере. Ожидание этого события (конец рисования в невидимый буфер) дополняется ожиданием завершения цикла прямого хода развертки экрана. Поэтому самая большая частота смены изображений равна текущему значению частоты кадров дисплея. Допустим, что эта частота равна 60 Гц, тогда частота смены изображений будет 60 fps (frames per second — кадров в секунду). Если время рисования занимает немногим более 1/60 с (пусть 1/45 с), то один и тот же кадр будет проецироваться два такта цикла развертки и частота смены изображений реально будет 30 fps. Промежуток времени между 1/30 с и 1/45 с процессор простаивает (is idle). Если время подготовки невидимого кадра нестабильно (плавает), то частота смены изображений может измениться скачком, что воспринимается как неприятная помеха. Для сглаживания этого эффекта иногда искусственно добавляют небольшую задержку, с тем чтобы немного снизить частоту, но сделать ее стабильной. Отметьте, что OpenGL не имеет команды переключения буферов, так как такая команда всегда зависит от платформы. Мы будем пользоваться функцией SwapBuf f ers(HDC hdc), входящей в состав Windows API.
Перспективная проекция
В ортографической проекции .(giuOrtho2D) мы, в сущности, создавали двухмерные изображения в плоскости z = 0. В других типах проекций (gldrtho и gluPerspective) можно создавать трехмерные изображения. Эффект реального трехмерного пространства достигается в проекции с учетом перспективы. Теперь мы будем пользоваться только этим режимом передачи. Другой режим glOrtho вы опробуете самостоятельно, так как я не вижу какой-либо интересной сферы его применения. Вставьте в обработчик WM_SIZE вместо строки:gluOrtho2D (0., double (w), 0., double(h) ) ;
строку:
gluPerspective(45., double(w)/double(h), 1., 100.);
В OpenGL для обозначения видимого объема используется термин frustum. Он имеет латинское происхождение и примерно означает «отломанная часть, кусок».
Frustum задается шестью плоскими границами типа (min, max) для каждой из трех пространственных координат. В перспективном режиме просмотра frustum — это усеченная пирамида, направленная на наблюдателя из глубины экрана. Все детали сцены, которые попадают внутрь этого объема, видны, а те, что выходят за него, — отсекаются конвейером OpenGL. Другой режим просмотра — ортографический, или режим параллельной проекции, задается с помощью функции glOrtho. Он не учитывает перспективу, то есть при увеличении (удалении) координаты Z объекта от точки, в которой располагается наблюдатель, размеры объектов и углы между ними не изменяются, что напоминает плоские проекции объекта. Первый параметр функции gluPerspective задает угол перспективы (угол обзора). Чем он меньше, тем больше увеличение. Вспомните школьные объяснения работы телескопа или бинокля, где были настройки фокусного расстояния, определяющего угол зрения. Последние два параметра задают переднюю и заднюю грани видимого объема или frustum'a. Он определяет замкнутый объем, за пределами которого отсекаются все элементы изображения. Смотри иллюстрации в MSDN / Periodicals / Periodicals 96 / Microsoft System Journals/November / OpenGL Without the Pain. Боковые грани фрустума определяются с учетом дисбаланса двух размеров окна (отношения double(w) / double(h)). Мы вычисляем его и подаем на вход функции в качестве второго параметра.
Вспомните и найдите функцию, в которой мы задавали размеры окна, и увеличьте вертикальный размер до 500, так как далее мы собираемся изображать более крупные объекты. Введите определения новых глобальных переменных:
//====== Углы поворотов изображения вокруг осей X и Y
double gdAngleX, gdAngleY; //====== Сдвиги вдоль координат
double gdTransX, gdTransY, gdTransZ = -4.;
С их помощью мы будем транслировать (перемещать) изображения в трехмерном пространстве и вращать их вокруг двух осей. Включите учет глубины, вставив вызов
glEnable(GL_DEPTH_TEST);
в функцию Init. Туда же вставьте установку режима заполнения полигонов
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
и уберите строку, задающую текущий цвет вершин
glColorSd (1., 0.4, 1.);
так как мы теперь будем задавать его в другом месте. При подготовке окна OpenGL и формата его пикселов надо установить бит AUX_DEPTH — учет буфера глубины. Замените существующий вызов функции auxlnitDisplayMode на: auxInitDisplayMode (AOX_SINGLE I AUX_RGB I AUX_DEPTH);
В функции перерисовки, приведенной ниже, мы создадим куб, координаты которого будем преобразовывать с помощью матрицы моделирования. Порядок работы с этой матрицей таков:
Предположим, например, что текущая (current) матрица С размерностью 4x4 равна единичной С = 1 и поступает команда glTranslated (dx, dy, dz);. Эта команда создает матрицу сдвига Т и умножает ее справа на текущую (единичную) матрицу (С = I*Т). Затем она вновь записывает результат в текущую матрицу С. Теперь текущая матрица приняла вид:
| 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 |
Команды вращения glRotate* и растяжения-сжатия glScale* действуют сходным образом. В функции onDraw, приведенной ниже, начальный поворот и последующие вращения вокруг оси Y осуществляются вызовом glRotated (gdAngleY, 0., l., 0.);. Аналогичный вызов glRotated (gdAngleX, 1., 0., 0.); вращает все точки примитивов вокруг оси X:
void _stdcall OnDraw()
{
glClear(GL_COLOR_BOFFER_BIT I GL_DEPTH_BUFFER_BIT);
//== Будем пользоваться услугами матрицы моделирования glMatrixMode
glLoadldentity ();
//=== Задаем смещение координат точек будущих примитивов glTranslated(gdTransX, gdTransY, gdTransZ);
//===Задаем вращение координат точек будущих примитивов
glRotated(gdAngleY, 0.,1.,0.);
glRotated(gdAngleX, 1.,0.,0.);
//====== Координаты точек куба (центр его в нуле)
static float v[8][3] =
{
-1, 1.,-1., //4 точки задней грани задаются
1., 1., -1., //в порядке против часовой стрелки
1-, -1-, -1.,
-1, -1., -1.,
-1, 1,, 1., //4 фронтальные точки
-1-, -1., 1.,
1, -1., 1.,
1, 1., 1.
};
//====== 6 нормалей для 6-ти граней куба
static double norm[6][3] =
{
0., 0., -1., // Rear
0., 0., 1., // Front
-1., 0., 0., // Left
1., 0., 0., // Right
0., 1., 0., // Top
0., -1., 0. // Bottom
};
//====== Индексы вершин
static GLuint id[6][4] =
{
0,1,2,3,// Rear (обход CCW - counterclockwise)
4,5,6,7, // Front
0,3,5,4, // Left
7,6,2,1, // Right
0,4,7,1, // Top
5,3,2, 6, // Bottom
};
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glColorSd (1., 0.4, 1.);
glBegin(GL_QUADS);
//====== Долго готовились - быстро рисуем
for (int i = 0; i < 6; i++)
{
glNormal3dv(norm[i]) ;
for (int j = 0; j < 4; j++)
glVertex3fv(v[id[i] [j]]);
}
glEndf) ;
glFlush ();
}
Запустите и отладьте приложение. Вы должны увидеть совсем плоский квадрат, несмотря на обещанную трехмерность объекта. Пока ничего вразумительного, никакого трехмерного эффекта. Закомментируйте или удалите (или измените на GL_SMOOTH) настройку glShadeModel (GL_FLAT), так как теперь мы хотим интерполировать цвета при изображении полигонов. Это работает при задании разных цветов вершинам. Попробуйте задать всем вершинам разные цвета.
Попробуйте покрутить изображение, изменяя значения переменных gdAngleX, gdAngleY. Например, вместо нулевых значений, присваиваемых глобальным переменным по умолчанию, задайте:
double gdAngleX=20, gdAngleY=20;
Посмотрите в справке смысл всех параметров функции glRotated и опробуйте одновременное вращение вокруг двух осей, задав большее число единиц в качестве параметров. Позже мы автоматизируем процесс сдвигов и вращений, а сейчас, пока мы не умеем реагировать на сообщения мыши, просто измените значение какого-либо угла поворота и запустите музыку. Объясните результаты. Попробуйте отодвинуть изображение, изменив регулировку gdTransZ. Объясните знак смещения.
Подготовка окна Подготовку контекста
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;
Подключаемые библиотеки Microsoft-реализация
glColorSb(GLbyte red, GLbyte green, GLbyte blue);определяет цвет тремя компонентами типа GLbyte, а функция
void glColor4dv(const GLdouble *v) ;
задает его с помощью адреса массива из четырех компонентов.
С учетом этих вариантов ядро библиотеки содержит более 300 команд. Кроме того, вы можете подключить библиотеку утилит GLU32.LIB, которые дополняют основное ядро. Здесь есть функции управления текстурами, преобразованием координат, генерацией сфер, цилиндров и дисков, сплайновых аппроксимаций кривых и поверхностей (NURBS — Non-Uniform Rational B-Spline), а также обработки ошибок. Еще одна, дополнительная (auxiliary) библиотека GLAUX.LIB позволяет простым способом создавать Windows-окна, изображать некоторые SD-объекты, обрабатывать события ввода и управлять фоновым процессом. К сожалению, эта библиотека не документирована. Компания Microsoft не рекомендует пользоваться ею для разработки коммерческих проектов, так как она содержит код цикла обработки сообщений, в который невозможно вставить обработку других произвольных сообщений.
Около двадцати Windows GDI-функций создано специально для работы с OpenGL. Большая часть из них имеет префикс wgl (аббревиатура от Windows GL). Эти функции являются аналогами функций с префиксом glx, которые подключают OpenGL к платформе X window System. Наконец, существует несколько Win32-функций для управления форматом пикселов и двойной буферизацией. Они применимы только для специализированных окон OpenGL.
Рекурсивное деление Добавим возможность
Split(double *vl, double *v2, double *v3,long depth){
double v12[3], v23[3], v31[3];
if (depth == 0)
{
//====== Рисование наименьших треугольников
setTria(vl, v2, v3);
//====== и выход из цепи рекурсивных вызовов
return;
}
//====== Разбиение треугольника
for (int i = 0; i < 3; i++)
{
v12[i] = vl[i]+v2[i];
v23[i] = v2[i]+v3[ij;
v31[i] = v3[i]+vl[i];
}
//====== Дотягивание до сферы
Scale(v12);
Scale(v23);
Scale(v31); //====== Рекурсивное разбиение на
//====== четыре внутренних треугольника
Split(vl, v!2, v31, depth-1);
Split(v2, v23, v12, depth-1);
Split(v3, v31, v23, depth-1);
Split(v!2, v23, v31, depth-1);
}
Внесите также изменение в ту строку программы, где происходит вызов Split. Надо добавить параметр, задающий глубину рекурсии. Если функцию вызвать с нулевой глубиной, то получим икосаэдр, если увеличивать глубину, то будем получать фигуры, более близкие к шару:
for (int i = 0; i < 20; i++)
Split (v[id[i) [0]], v[id[i][l]], v[id[i] [2]], 3);
Запустите и проверьте, нажимая клавишу N. Попробуйте изменить глубину рекурсии, только не переусердствуйте. Если задать глубину более 10, то можно не дождаться ответа. Рекурсия дорого стоит, поэтому исследованный подход абсолютно неприемлем для создания сферы. Аналогичный вывод справедлив для других объемных изображений, создаваемых с помощью задания вершин большого числа геометрических примитивов.
В данный момент для иллюстрации процесса приближения изображаемой фигуры к сфере напрашивается такой сценарий: пользователь нажимает клавишу — пробел, глубина рекурсии изменяется и изображение пересчитывается. Алгоритм управления глубиной рекурсии, очевидно, следует выбрать таким, чтобы, оставаясь в рамках допустимых значений, можно было проходить весь диапазон в обе стороны. Введите в функцию main обработку нажатия клавиши пробела:
auxKeyFunc(AOX_SPACE, KeySpace);
и создайте функцию обработки:
void _stdcall KeySpace()
{
//====== Флаг роста числа разбиений
static bool bGrow = true;
//====== Продолжаем разбивать до глубины 4
if (bGrow SS giDepth < 4)
{
giDepth += 1;
}
//====== Смена знака при глубине 4
else if (giDepth > 0)
{
bGrow = false;
giDepth == 1;
}
//====== Смена знака при глубине О
else
{
bGrow = true;
giDepth += 1;
}
DrawScene () ;
}
Алгоритм предполагает, что глобально определена переменная giDepth, которая хранит текущее значение глубины рекурсии. Добавьте к существующим глобальным переменным объявление:
//====== Глубина рекурсии
int giDepth;
В функции DrawScene замените параметр 3 (при вызове Split) на giDepth и запустите на выполнение.
//====== Если существует 1-й список,
if (gllsList(1))
//====== то освобождаем память
glDeleteLists (1,1);
Разъяснения можно найти в справке по функциям gllsList и glDeleteLists. He ошибитесь при выборе места вставки фрагмента, так как операции с памятью имеют особую важность. Запустите приложение и, нажимая на пробел, наблюдайте за изменением изображения, которое сначала приближается к сфере, затем постепенно возвращает свой первоначальный облик икосаэдра. Периодически нажимайте клавишу N для того, чтобы оценить влияние точного вычисления нормалей.
Штриховка линий Основные действия
OnDraw(){
//====== Стираем окно
glClear (GL_COLOR_BUFFER_BIT);
//====== Цвет фона (синеватый)
glColor3f (0.3f, 0.3f, 1.);
//== Рисуем сначала unstippled rectangle (без узора)
//== Rect - это тоже полигон
glRectf (20., 20., 115., 120.);
glColor3f (1., 0., 0.); // Меняем цвет на красный
glEnable (GL_POLYGON_STIPPLE); // Включаем штриховку
glPolygonStipple (gStrip); // Задаем узор
glRectf (120., 20., 215., 120.); // Рисуем
glColorSf (O.,0.,0.); // Меняем цвет на черный
glPolygonStipple (gSpade);
// Меняем узор glRectf (220., 20., 315., 120.);
glPolygonStipple (gStrip); // Меняем узор
glColor3f (0., 0.6f, 0.3f);
glRectf (320., 20., 415., 120.);
//== Готовимся заполнить более сложный, невыпуклый
//== (nоn convex) полигон
glPolygonStipple (gSpade);
glColorSd (0.6, O.f, 0.3f);
//======= Шесть вершин по три координаты
float c[6][3] =
{
420.,120.,0.,
420.,70.,0.,
470.,20.,0., 520., 70.,0.,
520.,120.,0.,
470.,100.,0.
};
//== Здесь мы специально выбираем nоn convex полигон,
//== чтобы увидеть как плохо с ним обходится OpenGL
glBegin (GL_POLYGON) ;
for (int i=0; i<6; i++)
glVertex3fv(c[i] ) ;
glEnd() ;
glDisable (GL_POLYGON_STIPPLE) ;
glFlush ();
}
Запустите и убедитесь в том, что последний полигон потерял одну точку. Затем замените цикл задания его вершин на:
for (int i=5; i>=0; i--) glVertex3fv(c[i]) ;
Здесь мы изменили порядок обхода вершин и начали с вогнутой вершины. Запустите и убедитесь в том, что теперь в полигоне есть все шесть вершин. OpenGL не гарантирует точную передачу вогнутых полигонов. Поэтому для надежной передачи их надо предварительно разбивать на выпуклые части. Если этими частями будут треугольники, то процесс разбиения называется tessellation (мозаика). Есть специальные функции для тесселяции полигонов. Их рассмотрение выходит за рамки этой книги. Попробуйте самостоятельно задать рассмотренный выше полигон в виде двух выпуклых четырехугольников. Для этого посмотрите справку по функции glBegin с параметром GL_QUADS.
Полигоны можно рисовать либо закрашенными (режим — GL_FILL), либо в скелетном виде (GL_LINE), либо в виде намеков (GL_POINT). Испробуйте все режимы на примере невыпуклой звезды. При рисовании точками попробуйте предварительно дать команду glPointSize (5):
void _stdcall OnDrawf)
{
glClear (GL_COLOR_BUFFER_BIT);
glColor3d (1., 0.4, 1.);
//=== 2 угла, характеризующие звезду и
//=== 2 характерные точки
double pi = 4. * atan(l.),
al = pi / 10., a2 = 3. * al,
xl = costal), yl = sin(al)',
x2 = cos(a2), y2 = sin(a2);
//=== Мировые координаты вершин нормированной звезды
double с[5][3] =
{
0., 1., 0.,
-х2, -у2, 0.,
xl, yl, 0.,
-xl, yl, 0.,
х2, -у2, 0.,
};
//====== Оконные координаты
for (int i=0; i<5; i+t)
{
c[i][0] = 250 + 100*c[i][0];
c[i][l] = 100 + 100*c[i] [1] ;
}
//=== Режим заполнения полигона - скелетный
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
//=== Задаем вершины полигона
glBegin(GL_POLYGON);
for (i=0; i<5; i++)
glVertex3dv(c[i] ) ;
glEnd() ;
glFlush() ;
}
Создание консольного проекта Для
//=====Макроподстановка для изображения одной линии
#define Line(xl,yl,x2,y2) \
glBegin(GL_LINES); \
glVertex2d ( (xl), (yl)); \
glVertex2d ((x2),(y2)); \
glEnd() ;
//====== Реакция на сообщение WM_PAINT
void _stdcall OnDraw()
{
//====== Стираем буфер кадра (framebuffer)
glClear (GL_COLOR__BUFFER_BIT) ;
//====== Выбираем черный цвет рисования
glColorSf (0., 0., 0.);
//=== В 1-м ряду рисуем 3 линии с разной штриховкой
glEnable (GL_LINE_STIPPLE);
glLineWidth (2.);
glLineStipple (1, 0x0101); // dot
Line (50., 125., 150., 125.);
glLineStipple (1, OxOOFF); // dash
Line (150., 125., 250., 125.);
glLineStipple (1, OxlC47); // dash/dot/dash
Line (250., 125., 350., 125.);
//====== Во 2-м ряду то же, но шире в 6 раз
glLineWidth (6.);
glLineStipple (1, 0x0101); // dot
Line (50., 100., 150., 100.);
glLineStipple (1, OxOOFF); // dash
Line (150., 100., 250., 100.);
glLineStipple (1, OxlC47); // dash/dot/dash
Line (250., 100., 350., 100.);
//== Во 3-м ряду 7 линий являются частями
//== полосы (strip). Учетверенный узор не прерывается
glLineWidth (2.);
glLineStipple (4, OxlC47); // dash/dot/dash
glBegin (GL_LINE_STRIP);
for (int i =1; i < 8; i++)
glVertex2d (50.*i, 75.); glEnd() ;
//== Во 4-м ряду 6 независимых, отдельных линий
//== Тот же узор, но он каждый раз начинается заново
for (i = 1; i < 7; i++)
{
Line (50*1, 50, 50* (i+1), 50);
}
//====== во 5-м ряду 1 линия с тем же узором
glLineStipple (4, OxlC47); // dash/dot/dash
Line (50., 25., 350., 25.);
glDisable (GL_LINE_STIPPLE); glFlush ();
}
//===== Реакция на WM_SIZE
void _stdcall OnSize (int w, int h)
{
glViewport (0, 0, (GLsizei) w, (GLsizei) h);
glMatrixMode (GL_PROJECTION); glLoadldentity();
//====== Режим ортографической проекции
gluOrtho2D (0.0, double(w), 0.0, double(h));
}
//====== Настройки
void Init()
{
//====== Цвет фона - белый
glClearColor (1., 1., 1., 0.);
//====== Нет интерполяции цвета при растеризации
glShadeModel (GL_FLAT); }
void main()
{
//=== Установка pixel-формата и подготовка окна OpenGL
auxInitDisplayMode (AUX_SINGLE | AUX_RGB);
auxInitPosition (200, 200, 550, 250);
auxInitWindow("My Stipple Test");
Init() ;
auxReshapeFunc (OnSize);
// Кого вызывать при WM_SIZE auxMainLoop(OnDraw);
// Кого вызывать при WM_PAINT
}
Функция main содержит стандартную последовательность действий, которые производятся во всех консольных приложениях OpenGL. С ней надо работать как с шаблоном приложений рассматриваемого типа. Первые три строчки функции main устанавливают pixel-формат окна OpenGL. Заботу о его выборе взяла на себя функция auxInitDisplayMode из вспомогательной библиотеки. В параметре мы указали режим использования только одного (front) буфера (бит AUX_SINGLE) и цветовую схему без использования палитры (бит AUX_RGB).
В функции init обычно производят индивидуальные настройки конечного автомата OpenGL. Здесь мы установили белый цвет в качестве цвета стирания или фона окна и режим заполнения внутренних точек полигонов. Константа GL_FLAT соответствует отсутствию интерполяции цветов. Вызов функции auxReshapeFunc выполняет ту же роль, что и макрос ON_WM_SIZE в MFC-приложении. Происходит связывание сообщения Windows с функцией его обработки. Все функции обработки должны иметь тип void _stdcall. Вы можете встретить и эквивалентное описание этого типа (void CALLBACK). Имена функций OnDraw и OnSize выбраны намеренно, чтобы напомнить о Windows и MFC. В общем случае они могут быть произвольными. Важно запомнить, что последним в функции main должен быть вызов auxMainLoop.
В OnSize производится вызов функции glviewport, которая задает так называемый прямоугольник просмотра. Мы задали его равным всей клиентской области окна. Конвейер OpenGL использует эти установки так, чтобы поместить изображение в центр окна и растянуть или сжать его пропорционально размерам окна. Аффинные преобразования координат производятся по формулам:
Xw=(X+1)(width/2)+X0
Yw=(Y+1)(height/2)+Y0
В левой части равенств стоят оконные координаты:
Как видно из подстановки в формулу, точка с координатами (0,0) попадет в центр окна, а при увеличении ширины или высоты окна (width или height) координаты изображения будут увеличиваться пропорционально. Вызов
glMatrixMode (GL_PROJECTION);
определяет в качестве текущей матрицу проецирования, а вызов glLoadldentity делает ее равной единичной матрице. Следующий за этим вызов
gluOrtho2D (0.0, double(w), 0.0, double(h));
задает в качестве матрицы преобразования матрицу двухмерной ортографической (или параллельной) проекции. Изображение будет отсекаться конвейером OpenGL, если его детали вылезают из границ, заданных параметрами функции gluOrtho2D.
Создание сферы Для иллюстрации
Разбиение сферы на треугольникиМы будем управлять степенью дискретизации сферы с помощью двух чисел: количества колец (gnRings) и количества секций (gnSects). Они определяют как полное количество вершин, так и треугольников. Если глобально зададим переменные:
const UINT gnRings = 20; // Количество колец (широта)
const UINT gnSects = 20; // Количество секций (долгота),то, так как каждый прямоугольник разбит на два треугольника, общее количество треугольников будет:
const UINT gnTria = (gnRings+1) * gnSects * 2;
//===Нетрудно подсчитать и общее количество вершин:
const UINT gnVert = (gnRings+1) * gnSects + 2;
Мы уже, по сути, начали писать код, поэтому создайте новый файл Sphere.срр и подключите его к проекту, а предыдущий файл OG.cpp отключите. Эти действия производятся так:
Введите в него директивы препроцессора, которые нам понадобятся, а также объявления некоторых констант:
#include
#include
#include
#include
#include
#include
#include
#include
#include
const UINT gnRings = 40; // Количество колец (широта)
const UINT gnSects = 40; // Количество секций (долгота)
//====== Общее количество треугольников
const UINT gnTria = (gnRings+1) * gnSects * 2;
//====== Общее количество вершин
const UINT gnVert = (gnRings+1) * gnSects + 2;
//====== Два цвета вершин
const COLORREF gClrl = RGB(0, 255, 0);
const COLORREF gClr2 = RGB(0, 0, 255);
const double gRad = 1.5; // Радиус сферы
const double gMax =5.; // Амплитуда сдвига
const double PI = atan(1.)*4,; // Число пи
Класс точки в 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 (глаз). Остальные индексы в применении к материалу обозначают различные компоненты его отражающих свойств.
Члены в круглых скобках — это скалярные произведения векторов. Если они дают отрицательные значения, то конвейер заменяет их нулем. Alpha-компонент результирующего цвета освещения устанавливается равным alpha-компоненту диффузного отражения материала. Так как мы задали лишь один источник света (LIGHTO), то знак суммы можно опустить. Обратите внимание на то, что блесткость материала уменьшает (обостряет) пятно отраженного света, так как возведение в степень h > 1 чисел (v, -v,), меньших единицы, уменьшает их значение. Параллельные векторы v, и v, дадут максимальный вклад. Чем больше их рассогласование, тем меньший вклад даст последний член формулы.
Ориентация поверхности
Кроме установки параметров света код функции init содержит довольно много других установок, которые мы осуществляем впервые, поэтому обсудим их более подробно. Возможно, вы помните из курса аналитической геометрии, что некоторые поверхности имеет ориентацию. По умолчанию поверхность любого полигона считается лицевой (FRONT), если вы задали ее обходом вершин против часовой стрелки, и она считается изнаночной (BACK), если направление обхода было обратным. В частности, ориентация поверхности влияет на ориентацию нормали.
Команда glEnable (GL_CULL_FACE); включает механизм учета ориентации поверхности полигонов. Она должна сопровождаться одним из флагов, определяющих сторону поверхности, например glCullFace(GL_BACK);. Таким образом, мы сообщаем конвейеру OpenGL, что обратные стороны полигонов можно не учитывать. В этом случае рисование полигонов ускоряется. Мы не собираемся показывать внутреннюю поверхность замкнутой сферы, поэтому эти установки нам вполне подходят.
Массив вершин, нормалей и цветов
Три команды glEnableClientstate говорят о том, что при формировании изображения будут заданы три массива (вершин, нормалей и цветов), а три команды вида gl* Pointer непосредственно задают адреса этих массивов. Здесь важно правильно задать не только адреса трех массивов, но и шаги перемещения по ним. Так как мы вместо трех массивов пользуемся одним массивом структур из трех полей, то шаг перемещения по всем трем компонентам одинаков и равен sizeof (VERT) — размеру одной переменной типа VERT. Массив вершин (vert типа VERT*) и индексов их обхода (Tria типа TRIA*) создается динамически внутри функции init. Характерно, что после того, как закончилось формирование списка рисующих команд OpenGL, мы можем освободить память, занимаемую массивами, так как вся необходимая информация уже хранится в списке. Формирование массивов производится в функции Sphere, которую еще предстоит разработать.
Далее по коду Init идет формирование списка рисующих команд. Так как массивы вершин и индексов их обхода при задании треугольников уже сформированы, то список рисующих команд создается с помощью одной команды glDrawElements. Ее параметры указывают:
Команды:
srandftime(0)); // Подготовка ГСЧ
gShift = CPoint3D(Rand(gMax), Rand(gMax), Rand(gMax));
позволяют задать характер вращения сферы. Константа const double gMax = 5.;
выполняет роль регулятора (ограничителя) степени подвижности сферы. Если вам захочется, чтобы сфера вращалась более резво, то увеличьте эту константу и перекомпилируйте проект.
Формирование массива вершин и индексов
Самой сложной задачей является правильное вычисление координат всех вершин треугольников и формирование массива индексов Tria, с помощью которого команда glDrawElements обходит массив Vert при задании треугольников. Функция Sphere реализует алгоритм последовательного обхода сначала всех сферических треугольников вокруг полюсов сферы, а затем обхода сферических четырехугольников, образованных пересечением параллелей и меридианов. В процессе обхода формируется массив вершин vert. После этого обходы повторяются для того, чтобы заполнить массив индексов Tria. Северный и южный полюса обрабатываются индивидуально. Для осуществления обхода предварительно создаются константы:
Для упрощения восприятия алгоритма следует учитывать следующие особенности, связанные с порядком обхода вершин:
void Sphere(VERT *v, TRIA* t)
{
//====== Формирование массива вершин
//====== Северный полюс
v[0].v = CPointSD (0, gRad, 0);
v[0].n = CPoint3D (0, 1, 0);
v[0].с = gClr2;
//====== Индекс последней вершины (на южном полюсе)
UINT last = gnVert - 1; //====== Южный полюс
v[last].v = CPointSD (0, -gRad, 0);
v[last].n = CPointSD (0, -1, 0) ;
v[last].c = gnVert & 1 ? gClr2 : gClrl;
//====== Подготовка констант
double da = PI / (gnRings +2.),
db = 2. * PI / gnSects,
af = PI - da/2.;
bf = 2. * PI - db/2.;
//=== Индекс вершины, следующей за северным полюсом
UINT n = 1;
//=== Цикл по широтам
for (double a = da; a < af; a += da)
{
//=== Координата у постоянна для всего кольца
double у = gRad * cos(a),
//====== Вспомогательная точка
xz = gRad * sin(a);
//====== Цикл по секциям (долгота)
for (double b = 0.; b < bf; n++, b += db)
}
// Координаты проекции в экваториальной плоскости
double х = xz * sin(b), z = xz * cos(b);
//====== Вершина, нормаль и цвет
v[n].v = CPointSD (x, у, z);
v[n].n = CPointSD (x / gRad, у / gRad, z / gRad);
v[n].c = n & 1 ? gClrl : gClr2; } }
//====== Формирование массива индексов
//====== Треугольники вблизи полюсов
for (n = 0; n < gnSects; n++)
{
//====== Индекс общей вершины (северный полюс)
t[n] .11 = 0;
//====== Индекс текущей вершины
t[n] .12 = n + 1;
//====== Замыкание
t[n].13 = n == gnSects - 1 ? 1 : n + 2;
//====== Индекс общей вершины (южный полюс)
t [gnTria-gnSects+n] .11 = gnVert - 1;
t tgnTria-gnSects+n] . 12 = gnVert - 2 - n;
t [gnTria-gnSects+n] .13 = gnVert - 2
t ( (1 + n) % gnSects) ;
}
//====== Треугольники разбиения колец
//====== Вершина, следующая за полюсом
int k = 1;
//====== gnSects - номер следующего треугольника
S' n = gnSects;
for (UINT i = 0; i < gnRings; i++, k += gnSects) {
for (UINT j = 0; j < gnSects; j++, n += 2) {
//======= Индекс общей вершины
t[n] .11 = k + j;
//======= Индекс текущей вершины
t[n].12 = k + gnSects + j;
//======= Замыкание
t[n].13 = k + gnSects + ((j + 1) % gnSects)
//======= To же для второго треугольника
t[n + 1].11 = t[n].11;
t[n + 1].12 = t[n].13;
t[n + 1J.13 = k + ((j + 1) % gnSects);
Для завершения работы осталось дополнить программу стандартным набором процедур, алгоритм функционирования которых вы уже изучили:
void_stdcall OnSize(GLsizei w, GLsizei h) { glViewport(0, 0, w, h);
}
void main ()
{
auxInitDisplayMode(AUX_RGB | AUX_DOUBLE) ;
auxInitPositiondO, 10, 512, 512);
auxInitwindow("Vertex Array");
Init() ;
auxReshapeFunc (OnSize) ;
auxIdleFunc (OnDraw) ;
auxMainLoop (OnDraw) ;
}
Запустите проект на выполнение и уберите возможные неполадки. Исследуйте функционирование программы, вводя различные значения глобальных параметров (регулировок). Попробуйте задать нечетное число секций. Объясните результат. В качестве упражнения введите возможность интерактивного управления степенью дискретизации сферы и исследуйте эффективность работы конвейера при ее увеличении.
Строим икосаэдр
Для иллюстрации работы с массивами вершин создадим более сложный объект — икосаэдр. Это такой дссятистенный дом с острой пятиугольной крышей и таким же полом, но углы пола смещены (повернуты) на л/5 относительно углов потолка.Икосаэдр имеет 20 треугольных граней и 12 вершин (1 + 5 на потолке и 1 + 5 на полу). Благодаря своей правильности он может быть задан с помощью всего лишь двух чисел, которые лучше вычислить один раз и запомнить. Этими числами является косинус и синус угла в три пятых окружности, то есть
static double
//====== atan(l.) - это пи/4
angle = 3. * atan(1.)/2.5, //====== 2 характерные точки
V = cos(angle), W = sin(angle);
Этот код мы вставим внутрь функции рисования, чтобы не плодить глобальные переменные и не нарываться на конфликты имен. Вот новая версия функции DrawScene:
void DrawScene() { static double
//====== 2 характерные точки
angle = 3. * atan(l.)/2.5, V = cos(angle), W = sin(angle),
//=== 20 граней икосаэдра, заданные индексами вершин
static GLuint id[20][3] =
(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) | |||||||
//====== Начинаем формировать список команд
glNewList (1,GL_COMPILE) ;
//====== Выбираем текущий цвет рисования
glColor3d (1., 0.4, 1 . ) ;
glBegin (GLJTRIANGLES) ;
for (int i = 0; i < 20; i++)
{
//====== Грубый подход к вычислению нормалей
glNorma!3dv(v[id[i] [0] ] ) ;
glVertex3dv(v[id[i] [0] ] ) ;
glNorma!3dv(v[id[i] [1] ] ) ;
glVertex3dv(v[id[i] [1] ] ) ;
glNorma!3dv(v[id[i] [2] ] ) ;
glVertex3dv(v[id[i] [2] ] ) ;
}
glEnd() ;
//====== Конец списка команд
glEndList ();
}
Точное вычисление нормалей
Проверьте результат и обсудите качество. В данном варианте нормали в вершинах заданы так, как будто изображаемой фигурой является сфера, а не икосаэдр. Это достаточно грубое приближение. Если поверхность произвольного вида составлена из треугольников, то вектор нормали к поверхности каждого из них можно вычислить точно, опираясь на данные о координатах вершин треугольника. Из $ курса векторной алгебры вы, вероятно, помните, что векторное произведение двух векторов а и b определяется как вектор п, перпендикулярный к плоскости, в которой лежат исходные векторы. Величина его равна площади параллелограмма, построенного на векторах а и b как на сторонах, а направление определяется так, что векторы a, b и п образуют правую тройку. Последнее означает, что если представить наблюдателя на конце вектора п, то он видит поворот вектора а к вектору b, совершаемый по кратчайшему пути против часовой стрелки. На рис. 6.4. изображена нормаль п (правая тройка) при различной ориентации перемножаемых векторов а и b.

Рис. 6.2. Ориентация вектора нормали
Если координаты векторов а и b известны, то координаты нормали вычисляю по следующим формулам. Длина вектора нормали п зависит от длин вектор сомножителей и величины угла между ними:
Nx=AxBz-AzBy
Ny=AzBx-AxBz
Nz=AxBy-AyBx
Чтобы нивелировать зависимость цвета вершины от амплитуды нормали, обыч вектор нормали масштабируют (или нормируют), то есть делают его длину р; ной единице, оставляя неизменным направление. С учетом сказанного создал две вспомогательные функции. Первая масштабирует, а вторая вычисляет н< маль к плоскости треугольника. Алгоритм вычисления использует координа двух сторон, прилегающих к текущей вершине треугольника:
//====Нормирование вектора нормали (или любого другого)
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 () ;
}
Вносим свет
Пока нет освещения, все попытки внести трехмерный реализм обречены на неудачу. Свет отражается по нормали (перпендикуляру) к поверхности. Однако в OpenGL нормаль надо задавать в вершинах, так как в случае произвольной криволинейной поверхности направление нормали различно в каждой ее точке. Чем точнее вычислен вектор нормали, тем реалистичней изображение. Но это дело довольно тонкое. Для тех, кто не любит математику, то есть излишне напрягать свое мышление, — просто отвратительное. Примеры с автонормалями расслабляют и усыпляют бдительность, так как они скрывают детали реализации. Чтобы с ними работать, тоже надо прилагать усилия и правильно включать вычислители (evaluators). Смотри документацию по функциям giMap*. В нашем же случае все просто. Нормали уже вычислены, осталось включить свет. Сделайте это, вставив изменения в тело функции init. Включите еще два параметра в конечном автомате (state machine) OpenGL.glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
Задайте некоторый поворот, например double gdAngleX=15, gdAngleY=30, и запустите на выполнение. Изображение должно стать значительно лучше, но куда делся цвет куба? Свет исключил цвет. Дело в том, что теперь цвет каждого пиксела вычисляется по формуле, которая учитывает цвет материала поверхности, его отражающие и испускающие свойства, цвет самого света, его направление и законы распространения (точнее, затухания — attenuation). По умолчанию OpenGL учитывает только направление света, но не место расположения источника. По умолчанию же свет направлен вдоль оси Z. Обратите внимание на то, что индекс 0 в GL_LIGHTO означает, что мы включаем первый из GL_MAX_LIGHTS возможных источников света. Эта константа зависит от платформы. Давайте определим ее для нашей платформы. Вставьте такой фрагмент:
int Lights;
glGetIntegerv(GL_MAX_LIGHTS, &Lights);
_asm nор
внутрь функции Init (после строки glEnable(GL_LIGHTO);) и поставьте точку останова (F9) на строке __asm пор.
Затем нажмите F5 (Go). Когда выполнение дойдет до точки останова, посмотрите в окно Variables и убедитесь в том, что Lights приняла значение 8. Если хотите, то используйте описанный прием в дальнейшем для выяснения многочисленных параметров и состояний OpenGL. Посмотрите справку по glGet, чтобы получить представление о количестве этих параметров. Теперь уберите отладочный код и включите еще один тумблер в машине состояний OpenGL — учет цвета материала. Для этого вставьте строку:
glEnable(GL_COLOR_MATERIAL) ;
в функцию Init и запустите приложение. Обратите внимание на отличие оттенков цвета разных граней. Они определяются OpenGL с учетом направления нормалей. Попробуйте изменить их направление и посмотрите, что получится.
Выбор способа вычисления нормалей
gbSmooth = false;которая будет помнить текущий способ вычисления нормалей, и сделаем так, чтобы каждое нажатие клавиши N инвертировало эту переменную и способ вычисления нормали. Введите в функцию main реакцию на нажатие клавиши N, вставив строку
auxKeyFunc(AUX_n, KeyN);
Реализацию функции обработки вставьте до функции main:
void _stdcall KeyN()
{
//====== Изменяем способ вычисления нормалей
gbSmooth = !gbSmooth;
11====== Заново создаем список команд
DrawScene(); }
Введите новую версию функции setTria, которая учитывает выбранный способ вычисления нормалей:
void setTria(double *vl, double *v2, double *v3)
{
glBegin(GLJTRIANGLES);
//====== Если выбран способ точного вычисления нормали
if (!gbSmooth)
{
//====== Правая тройка векторов
double dl[3], d2[3], norm[3];
//====== Вычисляем координаты векторов
//====== двух сторон треугольника
for (int j = 0; j.< 3; j++)
{
dl[j] = vl[j] - v2[j); d2[j] = v2[j] - v3[j];
}
//====== Вычисляем нормаль к плоскости
//====== треугольника со сторонами dl и d2
getNorm(dl, d2, norm);
glNormalSdv(norm);
glVertex3dv(vl);
glVertex3dv(v2);
glVertex3dv(v3);
}
else
{
//=== Неточное (приближенное) задание нормали
glNorma!3dv(vl);
glVertexSdv(vl);
glNorma!3dv(v2);
glVertex3dv(v2);
glNorraalSdv(v3);
glVertex-3dv(v3);
}
glEnd ();
}
Запустите и проверьте результат, нажимая клавишу N. Надеюсь, что теперь важность точного вычисления нормалей стала для вас еще более очевидной.
Учебник по Visual C++ .Net
Чтение данных В теле следующей
COGView: : ReadData (){
//=== Строка, в которую будет помещен файловый путь
TCHAR szFile[MAX_PATH] = { 0 } ;
//====== Строка фильтров демонстрации файлов
TCHAR *szFilter =TEXT ("Graphics Files (*.dat)\0")
TEXT("*.dat\0")
TEXT ("All FilesNO")
TEXT ( " * . * \ 0 " ) ;
//====== Выявляем текущую директорию
TCHAR szCurDir[MAX_PATH] ;
: :GetCurrentDirectory (MAX_PATH-1, szCurDir) ;
//== Структура данных, используемая файловым диалогом
OPENFILENAME ofn;
ZeroMemory (&ofn,sizeof (OPENFILENAME) ) ;
//====== Установка параметров будущего диалога
ofn.lStructSize = sizeof (OPENFILENAME) ;
и * . *, текстовые описания которых можно увидеть в одном из окон стандартного диалога поиска и открытия файлов:
//====== Функция непосредственного чтения данных
bool COGView: : DoRead ( HANDLE hFile) {
//====== Сначала узнаем размер файла
DWORD nSize = GetFileSize (hFile, 0) ;
//=== Если не удалось определить размер, GetFileSize
//====== возвращает 0xFFFFFFFF
if (nSize == 0xFFFFFFFF)
{
GetLastError () ;
MessageBox (_T ("Некорректный размер файла"));
CloseHandle (hFile) ;
return false;
//=== Создаем временный буфер размером в весь файл BYTE
*buff = new BYTE [nSize+1] ;
//====== Обработка отказа выделить память
if (Ibuff) {
MessageBox (_T ("Слишком большой размер файла"))
CloseHandle (hFile) ;
return false;
//====== Реальный размер файла
DWORD nBytes;
//====== Попытка прочесть файл
ReadFile (hFile, buff, nSize, &nBytes, 0) ; CloseHandle (hFile) ;
//====== Если реально прочитано меньшее число байт
if (nSize != nBytes)
{
MessageBox (_T ("Ошибка при чтении файла"));
return false;
}
//====== Генерация точек изображения
SetGraphPoints (buff, nSize) ;
//====== Освобождение временного буфера
delete [] buff;
// ====== Возвращаем успех
return true;
}
В данный момент можно запустить приложение, и оно должно работать. В окне вы должны увидеть изображение поверхности, которое приведено на рис. 7.1. Для создания рисунка мы изменили цвет фона на белый, так как в книге этот вариант считается более предпочтительным. Попробуйте изменить размеры окна. Изображение поверхности должно пропорционально изменить свои размеры. Оцените качество интерполяции цветов внутренних точек примитивов и степень влияния освещения. Позже мы создадим диалог для управления параметрами света и отражающих свойств материала. А сейчас отметим, что напрашивается введение возможности управлять ориентацией и местоположением поверхности с помощью мыши. Для того чтобы убедиться в сложности автомата состояний OpenGL, a также в том, что все в нем взаимосвязано, временно поменяйте местами две строки программы: glVertexSf (xi, yi, zi); и glVertex3f (xn, yn, zn);. Вы найдете их в теле функции DrawScene.

Рис. 7.1. Вид освещенной поверхности в 3D
Диалог по управлению светом В
Вид окна диалога по управлению параметрами светаОбратите внимание на то, что справа от каждого движка расположен элемент типа static Text, в окне которого будет отражено текущее положение движка в числовой форме. Три регулятора (элемента типа Slider Control) в левом верхнем углу окна диалога предназначены для управления свойствами света. Группа регуляторов справа от них поможет пользователю изменить координаты источника света. Группа регуляторов, объединенная рамкой (типа Group Box) с заголовком Material, служит для изменения отражающих свойств материала. Кнопка с надписью Data File позволит пользователю открыть файловый диалог и выбрать файл с данными для нового изображения. Для диалогов, предназначенных для работы в немодальном режиме, необходимо установить стиль Visible. Сделайте это в окне Properties > Behavior. Идентификаторы элементов управления мы сведем в табл. 7.1.
Таблица 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. и сравните качество генерируемой поверхности.
Подведем итог. В этой главе мы:


Рис. 7.5. Вид поверхности, освещенной слева


Рис. 7.6. Вид той же поверхности, но освещенной справа
График по умолчанию Пришла пора
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;
Подготовка изображения Разработаем
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х векторов боковых сторон в формуле для нормали (см. раздел «Точное вычисление нормалей» в предыдущем уроке).
Подготовка окна Вы помните, что
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, сх, су);
}
Работа с контейнером Для работы
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 Вспомните
//====== Правая кнопка не была нажатаm_bRightButton = false;
//====== Рисуем четырехугольниками
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
}
Реакция на сообщение о перерисовке
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) ;
}
Трехмерные графики функций
В этой главе мы разработаем Windows-приложение, которое в контексте OpenGL изображает трехмерный график функции, заданной произвольным массивом чисел. Данные для графика могут быть прочтены из файла, на который указывает пользователь. Кроме этого, пользователь будет иметь возможность перемещать график вдоль трех пространственных осей, вращать его вокруг вертикальной и горизонтальной осей и просматривать как в обычном, так и скелетном режим. Регулируя параметры освещения поверхности, пользователь может добиться наибольшей реалистичности изображения, то есть усилить визуальный эффект трехмерного пространства на плоском экране.
Графики могут представлять собой результаты расчета какого-либо физического поля, например поверхности равной температуры, давления, скорости, индукции, напряжения и т. д. в части трехмерного пространства, называемой расчетной областью. Пользователь объекта должен заранее подготовить данные и записать их в определенном формате в файл. Объект по команде пользователя считывает данные, нормирует, масштабирует и изображает в своем окне, внедренном в окно приложения-клиента. Пользователь, манипулируя мышью, управляет местоположением и вращением графика, а открыв окно диалога Properties, изменяет другие его атрибуты.
Управление изображением с помощью
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. Здесь можно воспользоваться стеком матриц моделирования. Теперь добавим код в заготовку функции реакции на сообщения таймера с тем, чтобы ввести фиксацию состояния вращения.
Установка цвета фона Введите вспомогательную
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) ;
}
Включаем анимацию Реакция на сообщение
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);
}
Запустите и протестируйте приложение. Скорректируйте, если необходимо, коэффициенты чувствительности.
Вспомогательный класс Нам вновь
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()
Ввод новых команд Вы заметили
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); и вновь проверьте работу приложения. Зубчатость края должна исчезнуть, но в алгоритме, тем не менее, осталась еще небольшая, слабо заметная неточность. Ее обнаружение и исправление я оставляю вам, дорогой читатель.

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

Рис. 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) ;
}
Учебник по Visual C++ .Net
Библиотека типов Для того чтобы
"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
Двойственные интерфейсы Технология
SomeMethod;(Поток сознания в скобках, по Джойсу или Жванецкому: новые концепции, новые технологии, глубина мыслей, отточенность деталей, настоящая теория должна быть красивой, тупиковая ветвь?, монополисты не только заставляют покупать, но и навязывают свой способ мышления, что бы ты делал без 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.
Напомню, что вся функциональность класса 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 OnFillColorChangedO
{
//====== Если выбран системный цвет,
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). В качестве упражнения решите эту проблему самостоятельно.
Фабрика классов Логика функционирования
!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) и проверьте его работу. Алгоритм проверки остается тем же, что и ранее, но здесь мы должны по логике разработчиков СОМ, радоваться тому, что выполняем большее число правил и стандартов, а также имеем возможность одновременно создавать несколько СОМ-объектов.
Файл описания DLL Для успешной
Для разработки минимального приложения, способного найти 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. Этот текст задан клиентским приложением, а воспринят и воспроизведен СОМ-объектом. Если объект не работает, то терпеливо проверьте все этапы создания сервера. В модели СОМ существует довольно много мест, где можно допустить ошибку. Наиболее вероятны ошибки в процессе регистрации.
Интерфейсы — основа СОМ-технологии
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 Разработчики
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», и повторите процедуру построения. После этого сервер готов к использованию.
Как работают СОМ-серверы Созданный
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.
Для того чтобы быть доступным тем приложениям, которые захотят воспользоваться услугами СОМ-объекта, сам класс тоже должен иметь дом (в виде 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 } }
Оно означает, что макрос создает структуру с именем
Модель программирования COM
Любой программный продукт представляет собой набор данных и функций, которые как-то используют, обрабатывают эти данные. Этот принцип, как вы знаете, лежит в основе ООП. Там класс инкапсулирует данные и методы, которые служат для управления ими. Сходный принцип используется и в модели программирования СОМ. СОМ-объектом (или OLE-объектом) называется такой программный продукт, который обеспечивает доступ к данным с помощью одного или нескольких наборов функций, которые называются интерфейсами.В отличие от ООП, которое рассматривает интеграцию классов на уровне исходных модулей — текстов программ, СОМ рассматривает интеграцию компонентов на двоичном уровне, то есть на уровне исполняемых модулей. Цель — многократное использование удачно разработанных компонентов именно на этом уровне. Двоичный уровень дает независимость от аппаратной архитектуры и языков программирования (взамен порождая массу других проблем). Двоичный стандарт взаимодействия позволяет СОМ-объектам, разработанным разными поставщиками и на разных языках, эффективно взаимодействовать друг с другом. С практической точки зрения СОМ — это набор системных библиотек (DLL-файлов), которые дают возможность разным приложениям, выполненных с учетом требований СОМ, взаимодействовать друг с другом. Исторически сложилось так, что СОМ состоит из нескольких различных технологий, которые пользуются услугами друг друга для формирования объектно-ориентированной системы. Каждая технология реализует определенный набор функций.
Преимуществами двоичных компонентов являются: взаимозаменяемость, возможность многократного использования, возможность параллельной разработки с последующей сборкой в одном проекте. Недостатки СОМ настолько очевидны, что я не буду их перечислять. Вы почувствуете их в тот момент, когда начнете самостоятельно разрабатывать свой первый СОМ-объект. Приведем далеко не полный список литературы, который поможет более детально разобраться в технологии СОМ.
СОМ реализует модель «клиент-сервер». Объекты, называемые серверами, предоставляют набор функций в распоряжение других объектов, называемых клиентами, но СОМ-объект может быть одновременно и клиентом, и сервером. Серверы всегда подчиняются спецификациям СОМ, в то время как клиенты могут быть как СОМ-объектами, так и не быть таковыми. Поставщик СОМ-объектов (сервер) делает объекты доступными, реализуя один или множество интерфейсов. Пользователь СОМ-объектом (клиент) получает доступ к объекту с помощью указателя на один или множество интерфейсов. С помощью указателя клиент может пользоваться объектом, не зная даже как он реализован и где он находится, но быть при этом уверенным, что объект всегда будет вести себя одинаково. В этом смысле интерфейс объекта представляет собой некий контракт, обещающий клиенту надежное поведение, несмотря на язык и местоположение клиента. Благодаря этому решается проблема бесконечных обновлений версий сервера. Новая версия сервера просто добавляет новые интерфейсы, но никогда не изменяет старых. Клиент может либо пользоваться новым интерфейсом, если он о нем знает, либо не пользоваться им, а вместо этого пользоваться старым. Добавление новых интерфейсов никак не влияет на работу клиентов, работающих со старыми. Кроме того, как нас уверяет документация, двоичный уровень делает компоненты независимыми от платформы клиента.
Независимость от языка Разработанный
Схема коммуникации клиент-серверСОМ не накладывает ограничений на структуру компонентов, он определяет лишь порядок их взаимодействия. В основе межпроцессной коммуникации лежит все та же косвенная адресация (таблица виртуальных функций), которая позволяет передать управление либо прямо методу интерфейса (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-файле, СОМ просто передаст клиенту указатель на фабрику классов сервера.
От сырых COM API к проекту ATL
В этом уроке мы научимся разрабатывать приложения, которые реализуют функции СОМ-сервера и СОМ-контейнера. Известная вам технология OLE (Object Linking and Embedding) базируется на модели COM (Component Object Model), которая определяет и реализует механизм, позволяющий отдельным компонентам (приложениям, объектам данных, элементам управления, сервисам) взаимодействовать между собой по строго определенному стандарту. Технология разработки таких приложений кажется довольно сложной для тех, кто сталкивается с ней впервые. Трудности могут остаться надолго, если не уделить достаточно времени самым общим вопросам, то есть восприятию концепции СОМ (Модель многокомпонентных объектов). Поэтому не жалейте времени и пройдите через все, даже кажущиеся примитивными, этапы развития СОМ-приложений, как серверов, так и контейнеров. Мы начнем с того, что создадим СОМ-сервер с помощью сырых (raw) COM API-функций для того, чтобы лучше понять механизмы взаимодействия компонентов. Эти механизмы будут частично скрыты в следующих приложениях, которые мы будем развивать на основе стартовых заготовок, созданных мастером Studio.Net в рамках возможностей библиотеки шаблонов ATL (Active Template Library).
Проект на основе ATL Библиотеки
"С" HRESULT _stdcallОписатель extern «С» означает, что при вызове функция будет использовать имя в стиле языка С, а не C++, то есть описатель отменяет декорацию имен, производимую компилятором C++ по умолчанию.
Функция DllCanUnloadNow определяет, используется ли данная DLL в данный момент. Если нет, то вызывающий процесс может безопасно выгрузить ее из памяти. Функция DllGetClassObject с помощью третьего параметра (LPVOID* ppv) возвращает адрес так называемой фабрики классов, которая умеет создавать СОМ-объекты, по известному CLSID — уникальному идентификатору объекта.
Откройте файл ATLGLJ.c и.убедитесь, что он пуст. Этот файл будет заполнен кодами компилятором MIDL, о котором мы уже говорили ранее. Запустите приложение (Ctrl+F5). Компилятор и компоновщик создадут исполняемый модуль типа DLL, но загрузчик не будет знать в рамках какого процесса (контейнера) следует запустить его на отладку.
В рамках тестового контейнера можно отлаживать работу элементов ActiveX, OLE-controls и других СОМ-объектов. Но сейчас наш объект еще не готов к этому, так как мы не создали СОМ-класса, инкапсулирующего желаемые интерфейсы. Поэтому закройте тестовый контейнер, вновь откройте в рамках Studio.Net уже существующий IDL-файл (Interface Description Language file) ATLGLidl и просмотрите коды, описывающие интерфейс, СОМ-класс и библиотеку типов. Вы помните, что этот файл обрабатывается компилятором MIDL, который на его основе создает другие файлы. Откройте файл ATLGM.c и убедитесь, что теперь он не пуст. Его содержимое было создано компилятором MIDL. В данный момент файл ATLGM.c содержит только один идентификатор библиотеки, который регистрируется с помощью макроподстановки MIDL_DEFINE_GUID.
Разработка клиента с использованием
"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();
}
Несмотря на то что здесь нет многих строчек кода, присутствовавшего в предыдущей версии клиентского приложения, новая версия тоже должна работать. Попробуем разобраться в том, как это происходит.
Вы можете увидеть результат воздействия директивы #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-указатели помогают обрабатывать ошибки и упрощают поддержку счетчиков ссылок.
Создание элемента типа ATL Control
Стартовая заготовка элемента 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);
};
Создание класса СОМ-объекта Подключите
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, MAXJLENGTH, 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
Уникальная идентификация объектов Данные типа
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), она будет кружиться в течение примерно полугода, при условии регулярного изучения СОМ-технологий.
Загадочные макросы Вернемся в
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}
Учебник по Visual C++ .Net
Файловые операции Создание тестовой
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.
Класс-оболочка Обычно при создании
//=========== COpenGL wrapper classclass COpenGL : public CWnd
{
protected:
DECLARE_DYNCREATE(COpenGL)
public:
//==== Метод для добывания CLSID нашего элемента
CLSID const& GetClsidO
{
static CLSID const clsid =
{
0x519d9ed8, Oxbc4'6, 0x4367,
{ Ox9c, OxcO, 0x49, 0x81, 0x40, Oxf3, 0x94, 0x16 }
};
return clsid;
}
virtual BOOL Create(LPCTSTR IpszClassName,
LPCTSTR IpszWindowName, DWORD dwStyle,
const RECT& rect, CWnd* pParentWnd, UINT nID, CCreateContext* pContext = NULL)
{
return CreateControl(GetClsid(), IpszWindowName,
dwStyle, rect, pParentWnd, nID)
}
BOOL Create (LPCTSTR IpszWindowName, DWORD dwStyle,
const RECT& rect, CWnd* pParentWnd, UINT nID, CFile* pPersist = NULL,
BOOL bStorage = FALSE, BSTR bstrLicKey = NULL)
{
return CreateControl(GetClsidO, IpszWindowName, dwStyle, rect, pParentWnd, nID, pPersist, bStorage, bstrLicKey);
}
//====== Методы, экспонируемые элементом ActiveX
public:
void SetFillColor(unsigned long newValue);
unsigned long GetFillColor();
void GetLightParams(long* pPos);
void SetLightParam(short Ip, long nPos);
void ReadData();
void SetFillMode(DWORD mode);
void GetFillMode(DWORD* pMode);
void GetQuad(BOOL* bQuad);
void SetQuad(BOOL bQuad);
};
Самым важным моментом в процедуре вставки класса является правильное задание CLSID того класса OpenGL, который был зарегистрирован в операционной системе при создании DLL-сервера, то есть нашего элемента ActiveX. He пытайтесь сравнивать те цифры, которые приведены в книге, с теми, которые были приведены в ней же до этого момента, так как в процессе отладки пришлось не раз менять как классы, так и целиком приложения. Мне не хочется отслеживать эти жуткие номера. Если вы хотите вставить правильные цифры, то должны взять их из вашей версии предыдущего приложения ATLGL. Например, откройте файл ATLGL.IDL и возьмите оттуда CLSID для ко-класса OpenGL, то есть найдите такой фрагмент этого файла:
[
uuid(519D9ED8-BC46-4367-9CCO-498140F39416),
helpstring("OpenGL Class") ]
coclass OpenGL
{
[default] interface IOpenGL;
[default, source] dispinterface _IOpenGLEvents;
};
И скопируйте первую строку
uuid(519D9ED8-BC46-4367-9CCO-498140F39416),
но с вашими цифрами и вставьте ее в качестве комментария в файл OpenGLh нового проекта TestGL. Затем аккуратно, соблюдая формат, принятый для структуры CLSID, перенесите только цифры в поля статической структуры clsid, которую вы видите в методе GetClsid класса-оболочки. Цифры должны быть взяты из принесенной строки, но их надо отформатировать (разбить) по-другому принципу. Например, для нашего случая правильным будет такое тело метода GetClsid:
CLSID const& GetClsidO
{
// Следующая строка взята из файла ATLGL.IDL
// 519D9ED8-BC46-4367-9CCO-498140F39416
static CLSID const clsid =
{
//======== Эти цифры взяты из файла ATLGL.IDL
0x519d9ed8, 0xbc46, 0x4367,
{ 0х9с, 0xc0, 0x49, 0x81, 0x40, 0xf3, 0x94, 0x16 } ) ;
return clsid;
}
Кроме этого важного фрагмента в новом классе объявлены два совмещенных метода Create, каждый из которых умеет создавать окно внедренного элемента ActiveX с учетом особенностей стиля окна (см. справку по CWnd: :CreateControl). Затем в классе-оболочке должны быть представлены суррогаты всех методов, экспонируемых классом OpenGL COM DLL-сервера ATLGL.DLL. В том, что вы не полностью приводите тела методов сервера, иначе это был бы абсурд, хотя и так близко к этому, можно убедиться, просмотрев на редкость унылые коды реализации класса-оболочки, которые необходимо вставить в файл OpenGLcpp. Утешает мысль, что в исправной Studio.Net эти коды не придется создавать и редактировать вручную:
#include "stdafx.h"
#include "opengl.h"
IMPLEMENT_DYNCREATE(COpenGL, CWnd)
//====== Стандартное свойство реализовано
//====== в виде пары методов Get/Set
void COpenGL::SetFillColor(unsigned long newValue)
{
static BYTE parms[] =
VTS_I4; InvokeHelper(0xfffffe02, DISPATCH_PROPERTYPUT,VT_EMPTY,
NULL, parms, newValue);
}
//====== Стандартное свойство
unsigned long COpenGL::GetFillColor0 {
unsigned long result;
InvokeHelper (Oxfffffe02, DISPATCH_PROPERTYGET, VT_I4, (void4)&result, NULL);
return result;
}
//====== Наши методы сервера
void COpenGL::GetLightParams(long* pPos)
{
static BYTE parms[] = VTS_PI4;
InvokeHelper (Oxl, DISPATCH_METHOD, VT_EMPTY, NULL,
parms, pPos);
}
void COpenGL: : SetLightParam (short lp, long nPos)
{
static BYTE parms [ ] = VTS 12 VTS 14;
InvokeHelper{0x2, DISPATCH_METHOD, VT_EMPTY, NULL,
parms, lp, nPos);
}
void COpenGL::ReadData()
InvokeHelper(0x3, DISPATCH_METHOD, VT_EMPTY, 0, 0) ;
void COpenGL::GetFillMode(DWORD* pMode)
static BYTE jparms[] =
VTS_PI4; InvokeHelper (0x4, DISPATCH_METHOD, VT_EMPTY, NULL,
parms, pMode);
}
void COpenGL::SetFillMode(DWORD nMode)
static BYTE parms[] =
VTS_I4;
InvokeHelper(0x5, DISPATCH_METHOD, VT_EMPTY, NULL, parms, nMode);
void COpenGL::GetQuad(BOOL* bQuad)
static BYTE parms[] =
VTS_PI4;
InvokeHelper(0x6, DISPATCH_METHOD, VT_EMPTY, NULL, parms, bQuad);
void COpenGL::SetQuad(BOOL bQuad)
static BYTE parms[] =
VTS_I4;
InvokeHelper (0x7, DISPATCH_METHOD, VT_EMPTY, NULL, parms, bQuad);
}
Затем подключите оба новых файла к проекту Project > Add Existing Item.
Конструируем облик страницы свойств
Вид новой вставки в блоке страниц свойств элемента ActiveXНа рисунке показано окно диалога в активном состоянии, но вам еще предстоит поработать, чтобы довести его до этого вида. Здесь очень важно не торопиться и быть внимательным. Опыт преподавания в MS Authorized Educational Center (www.Avalon.ru) подтверждает, что большая часть ошибок вносится на стадии работы с ресурсами. Визуальные редакторы расслабляют внимание, и ошибки появляются там, где вы меньше всего их ждете.
В основных чертах окно имеет тот же облик, что и окно диалога по управлению освещением сцены, разработанное ранее (в MFC проекте). Но здесь есть два новых элемента, функциональность которых ранее была спрятана в командах меню. Так как в рамках этого проекта мы не имеем меню, то нам пришлось использовать элементы управления, сосредоточенные в нижней части окна диалоговой вставки. Во-первых, не забудьте, что справа от каждого ползунка вы должны расположить элемент типа static Text, в окне которого будет отражено текущее положение движка в числовой форме.
Кнопка Выбор файла, как и ранее, позволяет пользователю открыть файловый диалог и выбрать файл с данными для нового изображения. Выпадающий список Заполнение позволяет выбрать режим изображения полигонов (GL_FILL, GL_POINT или GL_LINE), а кнопка Quads/Strip изменяет режим использования примитивов при создании поверхности. Идентификаторы элементов управления мы сведем в табл. 9.1.
Таблица 9.1. Идентификаторы элементов управления
Элемент | Идентификатор | ||
/ Диалог | IDD_PROPDLG | ||
Ползунок Общая в группе Освещенность | IDC_AMBIENT | ||
Ползунок Рассеянная в группе Освещенность | IDC_DIFFUSE | ||
Ползунок Отраженная в группе Освещенность | IDC_SPECULAR | ||
Text справа от Общая в группе Освещенность | IDC_AMB_TEXT | ||
Text справа от Рассеянная в группе Освещенность | IDC_DIFFUSE_TEXT | ||
Text справа от Отраженная в группе Освещенность | IDC_SPECULAR_TEXT | ||
Ползунок Общая в группе Материал | IDC_AMBMAT | ||
Ползунок Рассеянная в группе Материал | IDC_DIFFMAT | ||
Ползунок Отраженная в группе Материал | IDC.SPECMAT | ||
Text справа от Общая в группе Материал | IDC_AMBMAT_TEXT | ||
Text справа от Рассеянная в группе Материал | IDC_DIFFMAT_TEXT | ||
Text справа от Отраженная в группе Материал | IDC_SPECMAT_TEXT | ||
Ползунок Блестскость | IDC_SHINE | ||
Ползунок Эмиссия | IDC.EMISSION | ||
Text справа от Блестскость | IDC_SHINE_TEXT | ||
Text справа от Эмиссия | IDC_EMISSION_TEXT | ||
Ползунок X | IDC_XPOS | ||
Ползунок Y | IDC.YPOS | ||
Ползунок Z | IDC_ZPOS | ||
Text справа от X | IDC_XPOS_TEXT | ||
Text справа от¥ | IDC_YPOS_TEXT | ||
Text справа от Z | IDC_ZPOS_TEXT | ||
Выпадающий список Заполнение | IDC_FILLMODE | ||
Кнопка Quads | IDC.QUADS | ||
Кнопка Выбор файла | IDC_FILENAME | ||
Вместо кнопки Quads просится пара переключателей (radio button) Quads/Strip. Сначала я так и сделал, но потом, к сожалению, пришлось отказаться из-за сложностей введения отклика реакции или уведомления, на выбор, произведенный в группе переключателей. Они обусловлены несовершенством бета-версии Studio.Net. Если вы впервые устанавливаете группу переключателей (radio buttons), то вам следует знать, что группа Quads/Strip будет работать правильно, если числовые значения идентификаторов составляющих ее элементов следуют подряд и (только) для первого переключателя установлено свойство Group. Для второго этот флаг должен быть снят. Если вы вставляете еще одну группу, то картина должна повториться. Первый переключатель должен иметь свойство Group в положении True, а остальные (если их много) — нет.
Для того чтобы просмотреть числовые значения идентификаторов, следует поставить фокус на элемент IDD_PROPDLG в дереве ресурсов (в окне Resource View) и вызвать контекстное меню. Затем надо выбрать команду Resource Symbols. Появится диалог со списком всех идентификаторов, которые хранятся в файле resource.h. Не следует редактировать этот файл вручную.
Если, несмотря на предостережения, вам захочется изменить числовое значение какого-либо идентификатора, то можете это сделать в окне Properties.
Редактор ресурсов может с возмущением отвергнуть ваш выбор. Тогда ищите другой диапазон с помощью уже рассмотренного диалога Resource Symbols. Эта тактика потенциально опасна. Повторюсь и скажу, что проще удалить и создать заново весь ресурс. Однако если вы самостоятельно выработаете или узнаете о более надежной технологии, то прошу сообщить мне. В этот момент следует запустить сервер и проверить наличие элементов на новой странице свойств. Если что-то не так, надо внимательно проверить, а возможно, и повторить все шаги создания вкладки.
Подготовка сцены OpenGL Считая
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)
glEndO; }
//====== Закрываем блок команд GL_QUADS
if (m_bQuad) glEndO ;
//====== Закрываем список команд OpenGL
glEndList ();
}
Для осмысления алгоритма надо учитывать, что количество узлов сетки вдоль того или иного направления (X или Z) на единицу больше количества промежутков (ячеек). Кроме того, надо иметь в виду, что при расчете освещения OpenGL учитывает направление нормали (перпендикуляра) к поверхности. Реалистичность изображения во многом достигается благодаря аккуратному вычислению нормалей. Нормаль является характеристикой вершины (узла сетки).
Ручная коррекция класса Класс
#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)
Обзор класса 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) интерфейсы.
интерфейсы являются глазами и ушами СОМ-объекта, которые воспринимают сигналы из окружающего мира. Но некоторые объекты могут не только слушать, но и сказать нечто полезное. Это требует от клиента способности к диалогу. Двусторонний диалог
подразумевает наличие исходящих (outgoing) интерфейсов и особого механизма общения,
основанного на обработке событий (events), уведомлений (notifications) или запросов
(requests).
События и запросы сходны с Windows-сообщениями, которые также информируют окно о каком-то событии (WM_SIZE, WM_COMMAND) или запрашивают какие-то данные (WM_CTLCOLOR, WM_QUERYENDSESSION). Точки связи (connection points) предоставляются объектом для каждого исходящего из него интерфейса. Клиент, умеющий слушать, реализует эти интерфейсы с помощью объекта, называемого sink (сток, слив). Его можно представить себе в виде воронки, которую клиент подставляет для того, чтобы объект мог сливать в нее свои сообщения. С точки зрения стока исходящие (outgoing) интерфейсы являются входящими (incoming). Сток помогает клиенту слушать объект. Возможны варианты, когда одна воронка подставляется для восприятия интерфейсов от нескольких разных СОМ-объектов (multicasting) и когда один клиент предоставляет несколько воронок для восприятия интерфейсов от одного объекта.
Каждая точка соединения СОМ-объекта поддерживает интерфейс iConnect-ionPoint. С помощью другого интерфейса — iConnectionPointContainer — объект рекламирует клиенту свои точки связи. Клиент пользуется интерфейсом IConnectionPointContainer для получения информации о наличии и количестве исходящих интерфейсов или, что то же самое, точек соединения. Узнав о наличии IConnectionPoint, клиент использует его для передачи объекту указателя на свой сток или нескольких указателей на несколько стоков. Большинство, и Kraig Brockschmidt в том числе, отмечают, что все это довольно сложно усвоить сразу, поэтому не переживайте, если потеряли нить рассуждений в данной информации. Постепенно все уляжется.
Надо отметить, что в этой части СОМ используется наибольшее число жаргонных слов. Попробуем с их помощью коротко описать механизм, а также сценарий общения между клиентом и С О М-объектом при задействовании исходящих интерфейсов. Сначала объект беспомощен и не может сказать что-либо клиенту. Инициатива должна быть проявлена клиентом — контейнером СОМ-объекта. Он обычным путем запрашивает у сервера указатель на интерфейс IConnectionPointContainer, затем с помощью методов этого интерфейса (EnumConnectionPoints, FindConnectionPoint) получает указатель на интерфейс iConnectionPoint. Далее клиент использует метод Advise последнего интерфейса для того, чтобы передать объекту указатель на свой сток — воронку для слушания или слива сообщений. Начиная с этого момента объект имеет возможность разговаривать, так как он имеет воронку или указатель на интерфейс посредника в виде sink. Заставить замолчать объект может опять же клиент. Для этого он пользуется методом Unadvise интерфейса IConnectionPoint.
Излишняя сложность всей конструкции объясняется соображениями расширяемости (extensibility). Соединяемые объекты могут усложняться независимо от точек соединения, а точки связи могут развиваться, не принося тревог соединяемым объектам. Меня подобный довод не убедил, но мы должны жить в этом мире, каков бы он ни был.
Карта сообщений
Карта сообщений, которая должна вызвать у вас ассоциацию с картой сообщений MFC, содержит незнакомый макрос CHAIN_MSG_MAP. Он перенаправляет необработанные сообщения в карту сообщений базового класса. Дело в том, что ATL допускает существование альтернативных карт сообщений. Они определяются макросами ALT_MSG_MAP. Тогда надо использовать макрос CHAIN_ MSG_MAP_ALT. Мы не будем обсуждать эту тему более подробно. Следующий макрос — DEFAULT_ REFLECTION_HANDLER — обеспечивает обработчик по умолчанию (в виде DefWindowProc) для дочерних окон элемента ActiveX, которые получают отражаемое (reflected) сообщение, но не обрабатывают его.
Интерфейс ISupportsErrorlnfо
Поддержка этого интерфейса проста. В методе interfaceSupportsErrorinfo имеется статический массив а г г, в котором хранятся адреса идентификаторов вновь создаваемых интерфейсов, пока он у нас один HD_iOpenGL. В этом же методе осуществляется пробег по всему массиву индексов и вызов функции inlinelsEqualGUio, которая пока не документирована, но ее смысл может быть выведен из ее имени.
Интерфейс IViewObjectEx
Этот интерфейс является расширением интерфейса iviewobject2. Он поддерживает обработку объектов непрямоугольной формы. Например, их улучшенную (flicker-free — не моргающую) перерисовку, проверку попадания курсора внутрь объекта, изменение размеров и полу прозрачность объектов. Моргание при перерисовке возникает из-за того, что перед ней стирается все содержимое окна. Бороться с этим можно, например, так: рисовать в bitmap (растровый рисунок), не связанный с экраном, а затем копировать весь bitmap на экран одной операцией. Нас эта проблема не волнует, так как мы будем использовать возможности OpenGL. Видимо, можно отказаться от услуг этого интерфейса при оформлении заказа у мастера ATL. Макрос DECLARE_VIEW_STATUS задает флаги прозрачности объекта, определенные в структуре VIEWSTATUS. По умолчанию предложен набор из двух неразлучных флагов:
Макрос DECLARE_PROTECT_FINAL_CONSTRUCT защищает объект от удаления в случае, если внутренний (агрегированный) объект обнулит счетчик ссылок на наш объект. Метод CGomObjectRootEx: : FinalConstruct позволяет создать агрегированный объект с помощью функции CoCreatelnstance. Мы не будем пользоваться этой возможностью.
Карта объектов
В аналогичном проекте, созданном в рамках Visual Studio б, вы могли видеть карту объектов ов JECT_MAP, которая обеспечивает поддержку регистрации, инициализации и создания объектов. Карта объектов имеет привычную структуру:
BEGIN_OBJECT_MAP
OBJECT_ENTRY(CLSID_MyClass, MyClass)
END_OBJECT_MAP()
где макрос ов JECT_ENTRY вводит внутренний механизм отображений (тар) идентификаторов классов В их имена. При вызове функции CComModule; :RegisterServer она вносит в реестр записи, соответствующие каждому элементу в карте объектов. Здесь в рамках Studio.Net, вы видите другой макрос — OBJECT_ENTRY_AUTO, выполняющий сходную функцию, но при этом не нуждается в обрамлении из операторных скобок.
Создание контейнера на базе MFC
До сих пор для отладки и демонстрации нашего ActiveX-элемента мы пользовались услугами тестового контейнера ActiveX Control Test Container,который входит в состав инструментов Studio.Net. Пришла пора показать, как с помощью библиотеки классов MFC можно создать свой собственный простой контейнер, специализированный для управления элементом OpenGL Class.
Рис. 9.3. Внедрение элемента ActiveX в окно диалогового приложения
В отличие от Visual Studio б в конце этой процедуры в состав проекта (по умолчанию) не будет включен новый класс-оболочка (wrapper class) под именем CGpenGL. Такой класс необходим для дальнейшей работы с внедренным элементом ActiveX.
В документации бета-версии Studio.Net я нашел лишь намек на то, что wrapper-класс может быть создан с помощью ClassWizard. Однако мне не удалось добиться этого. Поэтому мы создадим класс-оболочку вручную. Конечно, здесь я использую заготовку класса, полученную в рамках Visual Studio 6. Она оказалась вполне работоспособной и в новой Studio.Net. Будем надеяться, что в следующих версиях Studio.Net рассмотренный процесс автоматического создания класса будет достаточно прозрачен.
Страницы свойств
Перед тем как мы начнем работать с окном СОМ-объекта, вводя в него реакции на управляющие воздействия, покажем, как добавить страницу свойств (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, которые могут не совпадать даже с предыдущей версией в этой книге, так как в процессе разработки сервера мне приходится неоднократно повторять заново процесс создания ко-классов:
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 загружается в пространство чужого процесса.
Переменная m_bDirty используется каркасом в качестве флага доступности кнопки Apply. Если m_bDirt у == FALSE; то кнопка недоступна. Она тотчас же должна стать доступной, если пользователь страницы диалога свойств введет изменения в органы управления на лице диалога. Конечно, этим состоянием управляет разработчик, то есть мы с вами.
Требования 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
В этом уроке мы продолжим разработку DLL-модуля, который после регистрации в системе в качестве СОМ-объекта позволит любому другому клиентскому приложению, обладающему свойствами контейнера объектов СОМ использовать его для отображения в контексте OpenGL трехмерного графика функции, заданной произвольным массивом чисел. Данные для графика СОМ-объект берет из файла, на который указывает пользователь клиентского приложения. Кроме этого, объект предоставляет клиенту возможность перемещения графика вдоль трех пространственных осей, вращения вокруг вертикальной и горизонтальной осей и просмотра как в обычном, так и скелетном режиме. Регулируя параметры освещения поверхности, пользователь может добиться наибольшей реалистичности изображения, то есть усилить визуальный эффект трехмерного пространства на плоском экране.
Графики могут представлять результаты расчета какого-либо физического поля, например поверхности равной температуры, давления, скорости, индукции, напряжения и т. д. в части трехмерного пространства, называемой расчетной областью. Пользователь объекта должен заранее подготовить данные и записать их в определенном формате в файл. Объект по команде пользователя считывает данные, нормирует, масштабирует и изображает в своем окне, внедренном в окно приложения-клиента. Пользователь, манипулируя мышью, управляет местоположением и вращением графика, а открыв стандартный диалог Properties, изменяет другие его атрибуты.
ATL (Active Template Library) — это библиотека шаблонов функций и классов, которая разработана с целью упрощения и ускорения разработки СОМ-объектов. Несмотря на заявления о том, что ATL не является альтернативой MFC, а лишь дополняет ее, побудительной причиной разработки этой библиотеки был тот факт, что объекты СОМ, разработанные с помощью MFC, и внедренные в HTML-документ, работали слишком медленно. Наследование от cobject и все те удобства, которые оно приносит, обходятся слишком дорого в смысле быстродействия, и в условиях web-страницы объекты MFC-происхождения проигрывают объектам, разработанным с помощью COM API. В библиотеке ATL не используется наследование от cobject и некоторые другие принципы построения классов, характерные для MFC. За счет этого удалось повысить эффективность работы СОМ-объектов и ускорить их функционирование даже в условиях web-страниц. Пользуясь справкой (Help), вы, наверное, видели, что многие оконные методы реализованы не только в классе cwnd, но и в классе cwindow. Последний является классом из иерархии библиотеки ATL, и именно он является главной фигурой при разработке окон СОМ-объектов.
Управление цветом фона Возможность
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 с помощью объекта m_ctrl. Перейдите в окно диалогового редактора и придайте окну диалога IDD_TESTGL_DIALOG.
Идентификаторы для элементов управления можно задать так, как показано в табл. 9.2.
Таблица 9.2. Идентификаторы элементов управления
Элемент | Идентификатор | ||
Диалог | IDD_TESTGL_DIALOG | ||
Кнопка Data File | IDCJILENAME | ||
Кнопка Back Color | IDC.BKCLR | ||
Переключатель Quads | IDC_QUADS | ||
Переключатель Strips | IDC_STRIPS | ||
Выпадающий список Fill Mode | IDC_FILL | ||
Ползунок Light (X) | IDC_XPOS | ||
Кнопка Close | IDOK |
Для кнопки Quads установите свойство Group в положение True, а для кнопки Strips — в False. Обе они должны иметь свойство Auto в состоянии True. Важно еще то, что числовые значения их идентификаторов должны следовать по порядку. Для кнопки Data File установите свойство DefaultButton. Для выпадающего списка снимите свойство Sort (сделайте его False) и слегка растяните вниз его окно в открытом состоянии, для этого сначала нажмите кнопку раскрывания. Для ползунка вы можете установить свойство Point в положение Top/Left. Обратите внимание на тот факт, что в режиме дизайна вы можете открыть с помощью правой кнопки мыши диалог со страницами свойств для элемента IDC_OPENGL, одну из которых мы создавали в предыдущем проекте. Теперь с помощью Studio.Net введите в диалоговый класс обработчики следующих событий:
Ниже мы приведем тела этих функций, а сейчас отметим, что все они пользуются услугами класса-оболочки для прямого вызова методов СОМ-сервера. Однако, как вы могли заключить из рассмотрения кодов класса COpenGL, на самом деле вызов будет происходить с помощью интерфейса IDispatch, а точнее его метода Invoke. Функция cwnd: : invokeHelper, вызов которой вы видите во всех методах COpenGL, преобразует параметры к типу VARIANTARG, а затем вызывает функцию Invoke. Если происходит отказ, то Invoke выбрасывает исключение.
В диалоговом классе мы попутно произвели упрощения, которые связаны с удалением ненужных функций OnPaint и OnQueryDragicon. Эти изменения обсуждались при разработке приложения Look. Во избежание недоразумений, которые могут возникнуть в связи с многочисленным ручным редактированием, приведем коды как декларации, так и реализации класса CTestGLDlg:
//=== Декларация диалогового класса (Файл TestGLDlg.h)
#include "opengl.h"
#pragma once
class CTestGLDlg : public CDialog
{
public:
CTestGLDlg(CWnd* p = NULL);
enum
{
IDD = IDD_TESTGL_DIALOG
};
//======= Объект класса-оболочки
COpenGL m_Ctrl;
//======= Запоминаем способ изображения
BOOL m_bQuads;
//======= Реакции на регуляторы в окне диалога
void OnSelchangeFill(void);
void OnClickedFilename(void);
afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
void OnCiickedBkcir(void);
void OnClickedQuads(void);
void OnClickedStrips(void);
protected:
virtual
void DoDataExchange(CDataExchange* pDX) ;
virtual BOOL OnlnitDialog();
afx_msg void OnSysCommand(UINT nID, LPARAM IParam);
DECLARE_MESSAGE_MAP()
};
В файл реализации методов класса мы кроме функций обработки сообщений от элементов управления вставили код начальной установки этих элементов. Для этой цели нам опять понадобилась связь с сервером, которую обеспечивает объект m_ctrl класса-оболочки. Характерным моментом является то, что обрабатываем событие WM_HSCROLL, которое поступает окну диалога, вместо того чтобы обработать уведомляющее событие NM_RELEASEDCAPTURE, которое идет от элемента типа Slider Control. Такая тактика позволяет реагировать на управление ползунком клавишами, а не только мышью:
#include "stdafx.h"
#include "TestGL.h"
#include "TestGLDlg.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = _FILE_;
#endif
//====== Пустое тело конструктора
CTestGLDlg::CTestGLDlg(CWnd* p) : CDialog(CTestGLDlg::IDD, p){}
void CTestGLDlg::DoDataExchange(CDataExchange* pDX) {
//====== Связывание переменной с элементом
DDX_Control(pDX, IDCJDPENGL, m_Ctrl);
CDialog::DoDataExchange(pDX);
}
//====== Здесь мы убрали ON_WM_PAINT и т. д.
BEGIN_MESSAGE_MAP(CTestGLDlg, CDialog) ON_WM_SYSCOMMAND()
//
}
}
AFX_MSG_MAP
ON_CBN_SELCHANGE(IDC_FILL, OnSelchangeFill)
ON_BN_CLICKED(IDC_FILENAME, OnClickedFilename)
ON_WM_HSCROLL()
ON_BN_CLICKED(IDC_BKCLR, OnClickedBkclr)
ON_BN_CLICKED(IDC_QUADS, OnClickedQuads)
ON_BN_CLICKED(IDC_STRIPS, OnClickedStrips)
END_MESSAGE_MAP()
//===== CTestGLDlg message handlers
BOOL CTestGLDlg::OnInitDialog()
{
//====== Добываем адрес меню управления окном
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu)
{
//====== Добавляем команду About
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING,
IDM_ABOUTBOX,"About...");
}
//====== Загружаем стандартный значок
HICON hMylcon = ::LoadIcon(0,(char*)IDI_WINLOGO);
Setlcon(hMylcon, TRUE); // Set big icon Setlcon(hMylcon, FALSE);
// Set small icon
CDialog::OnInitDialog();
//====== Начальная установка элементов
CComboBox *pBox = (CComboBox*)GetDlgltem(IDC_FILL);
pBox->AddString("Points"); pBox->AddString("Lines");
pBox->AddString("Fill"); pBox->SetCurSel (2);
//==== Выясняем состояние режима изображения полигонов
m_Ctrl.GetQuad(&m_bQuads);
WPARAM w = m_bQuads ? BST_CHECKED : BST_UNCHECKED;
//===== Устанавливаем состояние переключателя
GetDlgltem(IDC_QUADS)->SendMessage(BM_SETCHECK, w, 0);
w = m_bQuads ? BST_UNCHECKED : BST_CHECKED;
GetDlgltem(IDC_STRIPS)->SendMessage(BM_SETCHECK, w, 0);
return TRUE;
}
void CTestGLDlg::OnSysCommand(UINT nID, LPARAM iParam)
{
if ((nID S OxFFFO) == IDM_ABOUTBOX)
{
CDialog(IDD_ABOUTBOX).DoModal();
}
else
{
CDialog::OnSysCommand(nID, IParam);
}
}
//====== Выбор из списка типа Combo-box
void CTestGLDlg::OnSelchangeFill(void) "'*
{
DWORD sel = ((CComboBox*)GetDlgltem(IDC_FILL))->GetCurSel();
sel = sel==0 ? GL_POINT : sel==l ? GL_LINE
: GL_FILL;
m_Ctrl.SetFillMode(sel);
}
//==== Нажатие на кнопку запуска файлового диалога
void CTestGLDlg::OnClickedFilename(void)
{
m_Ctrl.ReadData();
}
//====== Реакция на сдвиг ползунка
void CTestGLDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
//====== Выясняем текущую позицию, которая не во
//====== всех случаях отражена в параметре nPos
nPos = ((CSliderCtrl*)GetDlgItem(IDC_XPOS))->GetPos() ;
m_Ctrl.SetLightParam (0, nPos);
}
//====== Запускаем стандартный диалог
void CTestGLDlg::OnClickedBkclr(void)
{
DWORD clr = m_Ctrl.GetFillColor() ;
CColorDialog dig (clr);
dig.m_cc.Flags |= CC_FULLOPEN;
if (dlg.DoModal()==IDOK)
{
m_Ctrl.SetFillColor(dlg.m_cc.rgbResult);
}
}
//====== Запоминаем текущее состояние и
//====== вызываем метод сервера
void CTestGLDlg::OnClickedQuads(void)
{
m_Ctrl.SetQuad(m_bQuads = TRUE);
}
void CTestGLDlg::OnClickedStrips(void)
{
m_Ctrl.SetQuad(m_bQuads = FALSE);
}
В настоящий момент вы можете запустить приложение, которое должно найти и запустить DLL-сервер ATLGL, генерирующий изображение по умолчанию и демонстрирующий его в окне внедренного элемента типа ActiveX. Сервер должен достаточно быстро реагировать на изменение регулировок органов управления клиентского приложения.
Подведем итог. В этом уроке мы научились:
Установка освещения Параметры
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, либо с помощью страницы свойств, которую мы вскоре создадим. В любом случае, эти методы должны либо получить параметр настройки изображения и перерисовать его с учетом настройки, либо вернуть текущее состояние запрашиваемого параметра настройки:

Рис. 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;
}
Введение методов в интерфейс IOpenGL
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. Некоторые детали этого процесса обсуждались в предыдущем уроке.
Введение обработчиков сообщений
}Отметьте, что прототип функции обработки отличается от того, который принят в MFC. Там он имеет вид af x_msg BOOL OnEraseBkgnd(CDC* pDC); и определен в классе CWnd. Наш класс COpenGL среди своих многочисленных предков имеет класс CComControl, который происходит от класса CWindowlmpl, а тот, в свою очередь, является потомком класса cwindow. Последний выполняет в ATL ту же роль, что и класс cwnd в MFC, но не несет с собой бремени наследования от CObject. Это в основном и ускоряет функционирование ATL-приложений.
Теперь введите в класс обработчик сообщения WM_CREATE и заполните его кодами, которые готовят окно и устанавливают некоторые параметры OpenGL:
LRESULT COpenGL::OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/,'LPARAM /*lParam*/, BOOL& bHandled)
//======= Описатель формата окна OpenGL
PIXELFORMATDESCRIPTOR pfd =
{
sizeof(PIXELFORMATDESCRIPTOR),
// Размер структуры
1,
// Номер версии
PFD_DRAW_TO_WINDOW |
// Поддержка
GDI PFD_SUPPORT_OPENGL |
// Поддержка OpenGL
PFD_DOUBLEBUFFER,
// Двойная буферизация
PFD_TYPE_RGBA,
// Формат RGBA, не палитра
24,
// Количество плоскостей
// в каждом буфере цвета
24, 0,
// Для компонента Red
24, 0,
// Для компонента Green
24, 0,
// Для компонента Blue
24, 0,
// Для компонента Alpha
0,
// Количество плоскостей
// буфера Accumulation
0,
// То же для компонента Red
0,
// для компонента Green
0,
// для компонента Blue
0, // для компонента Alpha
32, // Глубина Z-буфера
0, // Глубина буфера Stencil
0, // Глубина буфера Auxiliary
0, // Теперь игнорируется
0, // Количество плоскостей
0, // Теперь игнорируется
0, // Цвет прозрачной маски
0 // Теперь игнорируется
};
// Добываем дежурный контекст и просим выбрать ближайший
m_hdc = GetDCO ;
int iD = ChoosePixelFormat(m_hdc, &pfd) ;
if ( !ID )
{
ATLASSERT(FALSE);
return -1;
}
//====== Пытаемся установить этот формат
if ( ISetPixelFormat (m_hdc, iD, &pfd))
{
ATLASSERT(FALSE);
return -1;
}
//====== Пытаемся создать контекст передачи OpenGL
if ( !(m_hRC = wglCreateContext (m_hdc)))
{
ATLASSERT(FALSE);
return -1;
}
//====== Пытаемся выбрать его в качестве текущего
if ( !wglMakeCurrent (m_hdc, m_hRC))
{
ATLASSERT(FALSE);
return -1;
}
//====== Теперь можно посылать команды OpenGL
glEnable (GL_LIGHTING) ;
// Будет освещение
glEnable (GL_LIGHTO) ;
// Только 1 источник
glEnable (GL_DEPTH_TEST) ;
// Учитывать глубину (ось Z)
//====== Учитывать цвет материала поверхности
glEnable (GL_COLOR_MATERIAL) ;
//====== Устанавливаем цвет фона
SetBkColor () ;
bHandled = TRUE;
return 0;
}
Класс copenGL должен реагировать на сообщение WM_SIZE и корректировать видимый объем сцены. Мы будем использовать режим просмотра с учетом перспективы. Его определяет функция
gluPerspective. Введите в класс copenGL обработку WM_SIZE и вставьте в нее следующие
коды:
LRESULT COpenGL: :OnSize(UINT /*uMsg*/, WPARAM /*wParam*/,
LPARAM IParam, BOOL& bHandled)
{
// Распаковываем длинный параметр и узнаем размеры окна
UINT сх = LOWORD ( IParam) , су = HIWORD (IParam) ;
//====== Вычисляем максимальные диспропорции окна
double dAspect = cx<=cy ? double (су) /сх
: double (сх) /су;
//==== Задаем тип текущей матрицы (матрица проекции)
glMatrixMode (GL_PROJECTION) ;
//====== Приравниваем ее к единичной диагональной
glLoadldentity () ;
//== Параметры перспективы (45 градусов - угол обзора)
gluPerspective (45., dAspect, 1., 10000.);
glViewport (0, 0, сх, су); DrawScene {) ;
bHandled = TRUE;
return 0;
}
Функция glViewport, как вы помните, задает прямоугольник просмотра. При закрытии окна внедренного объекта необходимо освободить память, занимаемую контекстом передачи, и отказаться от услуг таймера, с помощью которого мы будем производить анимацию вращения изображения. Введите в класс обработчик сообщения WM_DESTROY и измените ее стартовый код, как показано ниже:
LRESULT COpenGL: :OnDestroy (UINT /*uMsg*/, WPARAM
/*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)
{
KillTimer(l);
if (m_hRC)
{
wglDeleteContext(m_hRC); m_hRC = 0;
}
bHandled = TRUE;
return 0;
}
Инициализация переменных
В конструктор класса вставьте код установки начальных значений переменных, с помощью которых пользователь сможет управлять сценой Open GL:
COpenGL: : COpenGL()
{
//====== Контекст передачи пока отсутствует
m_hRC = 0;
//====== Начальный разворот изображения
m_AngleX = 35. f;
m_AngleY = 20. f;
//====== Угол зрения для матрицы проекции
m_AngleView = 45. f;
//====== Начальный цвет фона
m_clrFillColor = RGB (255,245,255);
//====== Начальный режим заполнения
//====== внутренних точек полигона
m_FillMode = GL_FILL;
//====== Подготовка графика по умолчанию
DefaultGraphic ();
//=== Начальное смещение относительно центра сцены
//=== Сдвиг назад на полуторный размер объекта
m_zTrans = -1.5f*m_fRangeX;
m_xTrans = m_yTrans = 0.f ;
// Начальные значения квантов смещения (для анимации)
m_dx = m_dy = 0.f;
//=== Мыть не захвачена
m_bCaptured = false;
//=== Правая кнопка не была нажата
m_bRightButton = false;
//=== Рисуем четырехугольниками m_bQuad = true;
//====== Начальный значения параметров освещения
m_LightParam[OJ = 50; // X position
m_LightParam[l] = 80; // Y position
m_LightParam[2] = 100; // Z position
m_LightParam[3] = 15; // Ambient light
m_LightPararn[4] = 70; // Diffuse light
m_LightParam[5] = 100; // Specular light
m_LightParam[6] = 100; // Ambient material
m_LightParam[7] = 100; // Diffuse material
m_LightParam[8] = 40; // Specular material
m_LightParam[9] = 70; // Shininess material
m_LightParam[10] = 0; // Emission material
}
Функция перерисовки
Перерисовка изображения OpenGL состоит в том, что обнуляется буфер цвета и буфер глубины — буфер третьей координаты. Затем в матрицу моделирования (GL_MODELVIEW), которая уже выбрана в качестве текущей, загружается единичная матрица (glLoadldentity). После этого происходит установка освещения, с тем чтобы на него не действовали преобразования сдвига и вращения. Лишь после этого матрица моделирования домножается на матрицу трансляции и матрицу вращений. Чтобы рассмотреть изображение, достаточно иметь возможность вращать его вокруг двух осей (X и Y). Поэтому мы домножаем матрицу моделирования на две матрицы вращений (glRotatef). Сначала вращаем вокруг оси X, затем вокруг оси Y:
HRESULT COpenGL: :OnDraw (ATL_DRAWINFO& di)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glLoadldentity{);
//====== Установка параметров освещения
SetLight ();
//====== Формирование матрицы моделирования
glTranslatef(m_xTrans,m_yTrans,m_zTrans);
glRotatef (m_AngleX, l.0f, 0.0f, 0.0f );
glRotatef (m_AngleY, 0.0f, l.0f, 0.0f );
//====== Вызов рисующих команд из списка
glCallList(1);
//====== Переключение буферов
SwapBuffers(m_hdc);
return S_OK;
}
Взаимодействие классов Класс CPropDlg
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.
Учебник по Visual C++ .Net
Ассоциативные контейнеры
К ассоциативным контейнерам принадлежат: 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). Элементы в таком контейнере не упорядочены, хотя их можно добывать последовательно. Если вы вставите или удалите элемент, то последовательность оставшихся элементов может измениться, то есть она не гарантируется. Преимуществом рассматриваемого типа контейнеров является то, что в среднем они значительно быстрее сортированных ассоциативных контейнеров. Удачно подобранная функция хеширования позволяет выполнять вставки, удаления и поиск за постоянное, не зависящее от п, время. Кроме того, она обеспечивает равномерное распределение хешированных значений и минимизирует количество коллизий.
Если ключи не уникальны, то можно выбрать не hаsh_mар-контейнер, а контейнер типа hash_multimap. Если нужно просто хранить множество каких-то объектов, например строк текста, не ассоциируя их с другими объектами, то стоит подумать о контейнере типа hash_set. Ну а в случае, если среди этих объектов могут попасться одинаковые, то выбором может стать контейнер типа hash_multiset.
Хешируемые типы контейнеров все-таки упорядочивают контролируемую ими последовательность, вызывая встроенный в него объект hash Traits класса value_compare. Вы имеете доступ к этому объекту с помощью метода key_comp. В общем случае два элемента признаются либо одинаковыми, либо какой-либо из них признается меньшим. Но реальная упорядоченность элементов зависит от функции хеширования, функции, задающей отношение порядка и текущего размера хэш-таблицы, поэтому в общем случае невозможно предсказать порядок следования элементов. Важной характеристикой ассоциативных контейнеров является то, что вставка элементов не портит итераторы, а удаление делает недействительными только те итераторы, которые указывают на удаляемый элемент.
Использование 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)
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. В июле 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
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 Отображение
main (){
//========= Создаем отображение строки в целое
map
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;
Контейнеры типа priority_queue
<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
Как видно, объекты выстроены по убыванию возраста. Очереди и стеки допускают повторение элементов.
Контейнеры типа queue Очередь
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.frontO « "; ";
q.pop(); }
cout«"\n\n";
}
Поиск с помощью предиката Поиск
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
#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
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 Контейнеры
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