Cамоучитель по Visual Studio.Net

Классы приложения

Классы приложения

Раскройте элемент дерева классов под именем CMyView. Класс CMyView происходит от MFC-класса cview и дает вам возможность управлять обликами (views) документов в рамках модели программирования архитектуры «документ — представление», считающейся стандартной технологией разработки MFC-приложений. О ней достаточно много сказано. (См., например, Круглински Д. Основы Visual C++, М: «Русская редакция», 1997; Черносвитов A. Visual C++ и MFC, СПб.: «Питер», 2000.) Здесь мы также будем рассматривать особенности технологии, по позднее. А пока попробуем изменить коды нашего приложения так, чтобы оно умело отображать данные документа. Выполнив двойной щелчок над элементом дерева OnDraw (CDC *pDC), вы увидите новое окно-страницу, управляемое вкладкой MyView.cpp. Так именуется файл реализации (implementation file) класса CMyView. Курсор должен находиться на теле метода перерисовки:

void CMyView::OnDraw(CDC* pDC)
{
CMyDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
}

Здесь вместо подсказки // TODO: мы должны вставить код, отображающий данные документа. Функция OnDraw(CDC *pDC) входит в состав класса CMyView, являсь методом этого класса, и вызывается каркасом приложения в тех случаях, когда необходимо перерисовать окно, обслуживаемое классом CMyView.

Примечание
Примечание

Каркасом приложения (Application Framework) называется совокупность классов и других структур библиотеки MFC, которые присутствуют в вашем приложении неявно. Дело в том, что классы вашего приложения произведены (с помощью механизма наследования ООП) от классов MFC. Данные и методы этих классов компилятор включил в исполняемый модуль, и они работают на вас. (Данные используются, методы вызываются.) Но вы можете и не знать об этом.

Представлением документа называется клиентская область одного из окон-рамок, обслуживаемых классом CChildFrame и живущих внутри главного окна приложения. В MDI приложении их может быть много. Это те окна, которые можно видеть по очереди, все сразу, каскадом или рядом, не перекрывая друг друга (Cascade или Tile).

Перед тем как начать отображение данных, надо эти данные создать или добыть из класса, обслуживающего документ. В соответствии с концепцией архитектуры «документ — представление» все стратегические данные приложения должны храниться в классе документа, то есть в классе CMyDoc. Метод GetDocument (он вызывается в заготовке OnDraw) класса CMyView позволяет добыть указатель на объект класса CMyDoc, который управляет активным в данный момент документом.

Примечание
Примечание

Макроподстановка ASSERT_VALID в отладочной (Debug) версии проекта проверяет на осмысленность полученный указатель и дает сообщение об ошибке, в случае когда указатель равен нулю или в действительности не является адресом объекта класса, производного от класса CObject. Если вы просмотрите иерархию классов MFC, то увидите, что CObject является отцом-прародителем всех классов, потомки которых использованы в нашем приложении.

Итак, имея адрес документа, мы можем начинать отображение его данных в окне представления. В системе, поддерживающей графический интерфейс пользователя, все данные не просто выводятся на экран, они скорее «рисуются» в контексте устройства, связанном с окном. Подсистема 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-файл. Он читаем, и если открыть его в текстовом режиме, то можно увидеть предложения некоего служебного языка, описывающие состав и настройки рабочего пространства.


Контейнер точек

Контейнер точек

Зададимся целью нарисовать в логической системе координат плоский многоугольник (Polygon), координаты точек которого будем хранить в динамической структуре данных. Специалисты советуют в таких случаях пользоваться одним из множества шаблонов, реализующих поведение фундаментальных структур данных и присутствующих в рамках STL (Standard Template Library). Библиотека шаблонов STL доступна на любой платформе, так как является стандартом. Она станет доступной и нам, если мы подключим необходимый файл заголовков. Для хранения точек многоугольника мы выберем шаблон стандартного контейнера, который называется vector. Это динамическая структура данных, которая ведет себя как «умный» массив элементов произвольного типа. Любой контейнер удобно представлять себе в виде резиновой сумки с одинаковыми объектами любой природы, которая почти всегда полна и в которую всегда можно положить еще разумное количество объектов того же типа. Это возможно, потому что она растягивается, то есть способна динамически (на этапе выполнения) изменять свои размеры как в сторону увеличения, так и в сторону уменьшения.
Подробнее о контейнерах будет сказано позже, а сейчас надо решить, что должно в нем храниться. Так как многоугольник определяется координатами точек, то контейнер целесообразно «скроить» (по шаблону vector) так, чтобы в нем можно было хранить объекты класса CPoint. Этот класс является вспомогательным в MFC (не происходит от CObject). Найдите этот класс в иерархии классов библиотеки MFC. Для этого:
  • Дайте команду Help > Index.
  • В появившемся окне Index (Look for:) задайте CPoint.
  • В окне Index Results for CPoints — 3 topics found выберите строку CPoint Class (MFC).
  • Внизу появившегося окна CPoint Class найдите ссылку Hierarchy Chart и щелкните ее мышью.
  • Класс CPoint находится в правой части карты под заголовком Simple Value Types. После этого отыщите классы: CObject, CDocument, cview, cwnd, которые так или иначе присутствуют в каркасе нашего приложения. Закройте окна Index, Index Results и Hierarchy Chart.
    Теперь вы знаете, что CPoint содержит внутри себя две целые координаты (х, у) произвольной точки и множество полезных методов для управления точкой. Итак, мы решили хранить точки многоугольника (объекты класса CPoint) в контейнере, скроенном по шаблону vector. Параметр шаблона (в угловых скобках) указывает тип объектов, которые будут храниться в контейнере. Воспользуемся контекстным меню, возникающим при правом щелчке мыши (right-click) на имени класса CMyDoc в окне Class View. В этом меню:
  • Выберите команду Add > Add Variable. Появится диалог типа wizard (мастер).
  • В окне Variable Type диалога задайте тип нового объекта: vector
  • В окне Variable Name — имя: m_Points
  • Нажмите кнопку Finish.


  • Начало работы с Visual Studio Net

    Начало работы с 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. Вот другой способ сделать это:

  • открыть контекстное меню, щелкнув правой клавишей мыши над пустым местом планки обычного меню или над пустым местом стандартной инструментальной панели;
  • выбрать панель под именем Web;
  • нажать кнопку Ноmе на новой панели.
  • При поиске кнопок используйте всплывающие подсказки (tooltips).

    Обозревая окна Studio.Net, отметьте усовершенствования косметического характера: пункты меню теперь имеют значки, изменились цвета элементов интерфейса в разных состояниях, нарушив тем самым рекомендации Microsoft по созданию UI-элементов (User Interface).

    Примечание
    Примечание

    Visual C++, Microsoft Developer Network (MSDN), fj, Visual Basic и другие компоненты составляют интегрированную среду разработки приложений — Visual Studio Integrated Development Environment (IDE). Совместное использование одной и той же среды имеет очевидные преимущества, так как в процессе разработки приложений на разных языках можно пользоваться одними и теми же или сходными инструментами Studio.Net: Web Browser, Command Window, Tabbed Documents, редакторами кодов, HTML-страниц,XML-схем и редакторами ресурсов.



    Общий вид Studio Net

    Рисунок 1.1. Общий вид Studio.Net

    Общий вид Studio Net

    Окно диалога New Project

    Рисунок 1.2. Окно диалога New Project

    Окно диалога New Project



    Окно мастера Add Variable

    Рисунок 1.5. Окно мастера Add Variable

    Окно мастера Add Variable Просмотрите описание класса CMyDoc, дважды щелкнув на имени класса в окне Class View. В конце файла вы должны увидеть строку
    vector m Points;
    Теперь просмотрите тело конструктора класса. Для этого раскройте элемент дерева 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);
    }
    }

    Окно приложения My

    Рисунок 1.7. Окно приложения My

    Окно приложения 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 (многовато окон), и если вы действительно хотите понять алгоритм закрашивания кистью внутренних частей полигона, то вам придется немного потрудиться, даже имея хороший английский. К таким ситуациям тоже надо выработать правильное отношение. Программист должен быть кропотлив и терпелив.
    Подведем итог:
  • мы слегка затронули концепцию решений (solutions);
  • научились создавать начальную заготовку MFC-приложения;
  • немного привыкли к скользким как мыло окнам Visual Studio.Net;
  • вспомнили (или узнали) об архитектуре документ — представление;
  • ввели в документ фундаментальную структуру данных — контейнер точек, скроенный по шаблону (template) vector;
  • узнали кое-что о выводе в контекст устройства и координатных пространствах Windows;
  • получили первый опыт сражений с ошибками.


  • Окно Task List со списком ошибок

    Рисунок 1.6. Окно Task List со списком ошибок

    Окно 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.



    Представление классов в окне Class View

    Рисунок 1.4. Представление классов в окне Class View

    Представление классов в окне Class View

    Реакция на ошибки

    Реакция на ошибки

    Полезно откомпилировать и запустить приложение в незавершенном состоянии, так как это позволит увидеть, как проявляются ошибки и недомолвки. Процесс компиляции и сборки совместно называется построением (Build) проекта. Самым быстрым способом построить и запустить проект является ввод Ctrl+F5 и согласие с необходимостью повторения всего процесса.
    Ход процесса компиляции и сборки проекта освещается и комментируется Studio.Net в окне Output. Сообщения об ошибках, выявленных на стадии построения, также выводятся в этом окне, но по завершении процесса появится диалоговое окно с сообщением о наличии ошибок. Теперь вы должны выбрать: продолжать ли компоновку или нет. Разумным выбором будет No. Теперь сообщения об ошибках в более подробном виде появляются в окне Task List, которое имеет универсальный характер, но в нашем частном случае используется для отображения ошибок компиляции. Окно имеет вид списка, который помогает идентифицировать и локализовать ошибки.
    Выделив ошибку в списке, вы можете нажать F1 и получить по ней более подробную справку. В нашем случае, если не было ошибок ввода, вероятно, появятся более 10 ошибок, первую из которых приведем здесь:
    error C2143: syntax error : missing ';' before '<' C:\My Projects\My\MyDoc.h(38)
    Здесь мне хочется поговорить о том, как выудить из этих сообщений более или менее точное указание на истинное местоположение и причину ошибки или ошибок. Прежде всего надо психологически подготовиться к тому, что ошибки всегда и неизбежно будут преследовать вас. Если код содержит более 20 операторов, то он не может быть создан и введен без ошибок. Если же код содержит более 5000 операторов, то он всегда будет содержать их. Это почти аксиома (с долей иронии). Разработчик программного обеспечения вынужден большую часть жизни проводить за компьютером, бесконечно повторяя цепочку одних и тех же действий, которые составляют суть процесса отладки приложения. Чем спокойнее вы относитесь к своим ошибкам, тем быстрее вы с ними расправитесь.
    Итак, первое сообщение говорит нам о том, что отсутствует точка с запятой перед знаком ' <' в строке 38 файла с описанием класса CMyDoc. Сделайте двойной щелчок па этом сообщении и курсор в окне MyDoc.h перейдет на строку
    vector m_Points;
    Если у вас нет опыта, то полученное сообщение вряд ли раскроет вам причину ошибки. Но есть еще много других сообщений. Так как компилятор имеет свойство неоднократно и по-разному сообщать об одной и той же ошибке, то, возможно, придется проанализировать все сообщения, прежде чем признать себя побежденным. Studio.Net помогает вам тем, что сообщения об ошибках могут быть отсортированы по различным атрибутам, и это иногда является ключом к быстрейшей их локализации.



    Рисование в контексте устройства

    Рисование в контексте устройства
    Сейчас важно понять следующее. Пример моделирует ситуацию, когда мы имеем реальные World-координаты (термин, принятый в GDI) какого-то объекта, например разрез корабля, и хотим начертить его детали, которые всегда можно аппроксимировать многоугольниками в некоторой логической системе координат (например, листе ватмана размером 1000x1000 мм). При этом мы преобразуем реальные вещественные координаты корабля в логические, то есть целого типа, так как мы не можем чертить с погрешностью менее 1 мм. В соответствии с концепцией рисования в контексте устройства именно эти (логические) координаты мы и должны использовать в функциях рисования. При последующем выводе рисунка на экран или принтер операционная система автоматически преобразовывает каждую его точку в аппаратные (device) координаты, зависящие от типа и возможностей устройства вывода. Таким образом, мы имеем дело с тремя системами координат и двумя их преобразованиями.

    Примечание
    Примечание

    Если вы посмотрите справку по теме Coordinate Spaces and Transformations (Пространства и преобразования координат), то вы увидите, что в GDI рассматриваются четыре координатных пространства: World, Page, Device и Physical device, однако часто можно использовать только два (Page и Device). При этом пространства World и Page считаются одним логическим координатным пространством, а пространства Device в Physical device — физическим. Преобразование из пространства Device в Physical device ограничивается только подстройкой начала координат при отображении рисунка на каком-то конкретном устройстве вывода.

    Вызовите в окно редактора функцию On Draw. Для этого снова щелкните вкладку MyView.cpp группы окон, вероятно, слева и введите изменения в соответствии со следующим фрагментом:

    void 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);
    }

    Окно мастера MFC Application Wizard

    Рисунок 1.3. Окно мастера MFC Application Wizard

    Окно мастера MFC Application Wizard
  • Нажмите ОК и проанализируйте предлагаемые по умолчанию настройки проекта, которые определены в появившемся окне диалога, обслуживаемом инструментом Studio.Net под именем MFC Application Wizard.
  • Нажмите кнопку Finish.
  • Мы отложим разговор о различных типах шаблонов (стартовых заготовках) 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.

    Примечание
    Примечание

    Неуверенность относительно местоположения окон объясняется тем, что окна Studio.Net проявляют удивительную подвижность. При желании вы можете разместить их в разных группах tabbed-окон или сделать их свободными (floating), причаливаемыми (docable) или скрыть (hide). Поэкспериментируйте с командами (Docable, Hide, Floating, Auto Hide контекстного меню, которое появляется при щелчке правой клавишей мыши над заголовками окон.

    Опробуйте также команды меню 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).



    Создание нового проекта

    Создание нового проекта
    При создании нового проекта Studio.Net автоматически создает рабочее пространство и помещает в него этот проект. Вот перечень шагов для создания нового проекта и нового рабочего пространства (solution), его содержащего.

  • В меню File > New выберите команду Project.
  • В появившемся окне диалога New Project, в окне Project Type раскройте узел дерева под именем Visual C++ Projects и выберите узел Win32 Projects.
  • В окне Templates выберите тип проекта MFC Application.
  • В окне Name задайте имя проекта My.
  • В окне Location задайте или оставьте без изменения местоположение новой папки с файлами рабочего пространства.


  • Cамоучитель по Visual Studio.Net

    Изображение объекта в режиме MM_SOTROPIC

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

    Изображение объекта в режиме MM_SOTROPIC



    Масштабирование изображения

    Масштабирование изображения

    Зададимся целью внести изменения в приложение My, которое мы создали в предыдущей главе, таким образом, чтобы изображение геометрической фигуры всегда было в центре окна и следило за изменением размеров окна приложения, меняясь пропорционально. Напомним, что фигурой является многоугольник с произвольным количеством вершин, и он выводится в контекст устройства Windows с помощью объекта класса CDC. Для того чтобы упростить процесс слежения за размерами окна представления, введем в число членов класса CMyView новую переменную, которая будет хранить текущие размеры окна.
  • Вызовите контекстное меню над именем класса CMyView в окне Class View и выберите команду Add > Add Variable.
  • Заполните поля окна мастера так, чтобы он создал private-переменную m_s zView типа csize.
  • Убедитесь в том, что мастер Add Variable выполнил свою работу. В окне Class View вы должны увидеть ее в числе данных класса.
  • Повторите эти же действия и введите в состав CMyView private-переменную UINT m_nLogZoom, в которой мы будем хранить коэффициент увеличения, используемый при переходе из пространства World в пространство Page.
  • В пространстве Page мы увеличим изображение в 100 раз. Измените уже созданный В конструкторе CMyView инициализатор m_nLogZoom (0) на m_nLogZoom (100).
  • Примечание
    Примечание

    Если вы помните (об этом я уже говорил в связи с конструктором документа), наш многоугольник «в миру» задан вещественными координатами и имеет размах в 2 единицы, так как мы вписали его в окружность единичного радиуса, то есть диаметром в 2 единицы. Там же мы преобразовали все координаты в логическую систему, увеличив размеры многоугольника в 100 раз и перевернув его (так как ось Y экрана направлена вниз). Кроме того, мы сдвинули изображение вправо и вниз, чтобы попасть в центр «листа ватмана». Логические координаты (уже целого типа) мы занесли в массив m_Points, который использован в функции OnDraw класса представления при изображении многоугольника. Теперь надо изменить ситуацию. Обычно документ хранит истинные («мировые») координаты объектов, а класс представления преобразовывает их в логические и изображает в физическом устройстве с помощью рассмотренных преобразований. Так делают, потому что пользователя не интересуют логические (Page) координаты. Он должен видеть и иметь возможность редактировать реальные (World) координаты объекта.

    Чтобы реализовать указанный подход, надо заменить в классе документа массив целых координат на массив вещественных, а в классе CMyView создать еще один массив, но уже целых координат. Используя уже знакомую технику, введите в состав класса CMyView private-переменную
    vector m_Points;
    Ее имя совпадает с именем массива координат в документе, но это не помеха, если используешь ООП. Классы ограничивают область действия имен, скрывая их. В интерфейсе класса документа (файл MyDoc.h) замените объявление
    vector m_Points;

    на

    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 > VECPTS;
    Вставьте эту строку сразу после объявления класса 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.
  • Выделите имя класса CMyView в окне Class View и перейдите на страницу Properties, выбрав соответствующую вкладку.
  • Обратите внимание на панель инструментов окна Properties. Она динамически изменяется в зависимости от выбора (selection) в других окнах. Сейчас на ней должна быть кнопка с подсказкой Messages. Нажмите эту кнопку.
  • В появившемся списке сообщений найдите сообщение WM_S IZE. В правой ячейке (типа Combo box) таблицы выберите OnSize.
  • Вновь перейдите в окно Class View, найдите новую функцию-обработчик OnSize в составе класса CMyView и сделайте на ней двойной щелчок.
  • Фокус ввода переходит в окно редактора текста для файла MyView.cpp. Введите изменения так, чтобы функция приобрела вид:
  • 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) к окну. Напомним также, что документ имеет и поддерживает динамический список всех своих представлений.
  • В окне Class View поставьте курсор на имя класса CMyView и щелкните правой клавишей мыши.
  • Перейдите в окно Properties, щелкнув вкладку, с помощью подсказок отыщите на панели инструментов именно этого окна кнопку Overrides и нажмите ее.
  • Появится длинный список виртуальных функций родительских классов, которые можно переопределить в классе ему view. Найдите в нем функцию OnlnitialUpdate и выберите в правой половине таблицы действие .
  • Результат ищите в конце файла 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.



    Режимы отображения координат

    Режимы отображения координат

  • Масштабирование изображения
  • В 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 или одним из его потомков. Для выбора режима отображения используются методы этого класса:
  • SetMapMode — задание режима отображения координат;
  • SetWindowOrg — задание выделенной точки (начала отображения) в логической системе координат;
  • Setviewportorg — задание выделенной точки (начала отображения) в физической системе координат;
  • SetwindowExt — характеристика протяженности окна вдоль двух логических координат;
  • SetviewportExt — характеристика протяженности окна вдоль двух физических координат.
  • Восемь существующих режимов отображения координат задаются с помощью символьных констант, определенных в файле 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 направлена снизу-вверх. В двух последних режимах преобразование координат из логической системы в физическую выполняется в соответствии с формулами, которые приведены ниже. При этом используются следующие обозначения:

  • Viewport — область вывода, задаваемая физическими координатами;
  • Window — окно, задаваемое логическими координатами.
  • Выделенные точки 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-арифметикой).

    Изображение объекта в режиме MM_ANISOTROPIC

    Рисунок 2.2. Изображение объекта в режиме MM_ANISOTROPIC

    Изображение объекта в режиме 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 направленной вверх.

    Cамоучитель по Visual Studio.Net

    Анализ стартовой заготовки

    Анализ стартовой заготовки

    Первые две строки являются директивами препроцессора, которые сообщают ему, что до того, как начать процесс компиляции модуля, следует вставить в указанное место файлы заголовков (stdafx.h и API.h). Первый файл мы обсуждали в уроке 1. Он содержит директивы подключения библиотечных файлов-заголовков. Директива
    //======== Исключает редко используемые элементы

    //======== Windows-заголовков

    #define 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).

    Примечание
    Примечание

    Этот текст представляет собой строку символов «API», которая хранится в ресурсах. Ее можно увидеть, раскрыв дерево ресурсов в окне Resource View, узел String Table и дважды щелкнув на элементе String Table (group). С помощью этой строки ваше приложение регистрируется в операционной системе.
    При вызове функции winMain система передает ей параметры:
  • hinstance — описатель экземпляра приложения. Это адрес приложения, загруженного в память. В Windows NT/2000 этот адрес для всех приложений имеет одно и то же значение 0x00400000 (4 Мбайт);
  • hPrevlnstance — описатель предыдущего экземпляра приложения. Этот параметр устарел и теперь не используется в приложениях Win32;
  • lpCmdLine — указатель на командную строку. Мы не будем использовать этот параметр;
  • nCmdShow — состояние окна при начальной демонстрации.
  • Ранее в Win 16 второй параметр использовался в целях экономии ресурсов, но в Win32 — это NULL, так как каждый экземпляр приложения теперь выполняется в своем собственном виртуальном адресном пространстве процесса емкостью 4 Гбайт. Все экземпляры процесса загружаются начиная с одного и того же адреса в этом пространстве (см. последний урок). Теперь рассмотрим алгоритм функции WinMain:

  • она загружает из ресурсов две рассмотренные выше строки текста;
  • создает, заполняет и регистрирует структуру типа WNDCLASS;
  • создает главное окно приложения;
  • загружает клавиатурные ускорители;
  • запускает цикл ожидания и обработки сообщений.
  • Основные атрибуты главного окна приложения задаются в структуре типа 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 нигде явно не вызывается, хотя именно она выполняет всю полезную работу. Для проверки усвоения прочитанного ответьте на вопрос: «Когда и кем она вызывается?»

    Геометрическое перо

    Геометрическое перо

    Косметические перья работают значительно быстрее, чем другие, но это имеет значение только для сложных рисунков. Геометрическое перо может иметь любую толщину и любые атрибуты Windows-кисти (dither и pattern). Введем дополнения, которые позволят исследовать свойства геометрического пера. В число локальных переменных функции WndProc введите новые сущности:
    //====== Узоры штрихов (hatch) кисти, на основе
    //====== которых будет основано перо
    static 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 пиксел выдерживает подобное испытание и его можно использовать для графиков функций.

    Косметическое перо

    Косметическое перо

    Сначала исследуем косметическое перо. Некоторые его стили, задаваемые символьными константами, занесем в массив. Введем внутрь тела оконной процедуры (после объявления CustColors) объявления новых локальных переменных:
    //====== х-координаты:

    static int 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, которая состоит из последовательности следующих шагов:

  • создаем свой инструмент;
  • выбираем его в контекст устройства (SelectObject) и одновременно запоминаем тот инструмент, который используется в контексте в настоящий момент;
  • рисуем с помощью нашего инструмента;
  • возвращаем в контекст прежний инструмент;
  • освобождаем память, занимаемую нашим инструментом.
  • Так как система работает с ресурсами 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 ());

    Меню и диалог

    Меню и диалог

    При выборе пользователем какой-либо команды меню система посылает в оконную процедуру сообщение WM_COMMAND, в коротком (wParam) параметре которого будет спрятан идентификатор команды. В обработке сообщения WM_COMMAND содержится распаковка короткого параметра и разветвление в зависимости от идентификатора команды. В ответ на команду About вызывается диалог, шаблон которого вы можете найти в ресурсах приложения.
    Запуск диалога в модальном режиме обеспечивает API-функция DialogBox, последним параметром которой является адрес функции About. Он явно приводится к типу DLGPROC (диалоговые процедуры). Этот тип определен как указатель на функцию обратного вызова (реакцию на сообщение) с определенным прототипом. Функция About будет вызываться системой для обработки сообщений, посылаемых уже не главному окну приложения, а окну диалога. Отметим, что описатели CALLBACK, WINAPi и FAR PASCAL идентичны. Они появились на разных этапах развития Windows и одновременно используются системой для обеспечения совместимости со старыми версиями.
    Параметры функции About имеют следующий смысл:
  • HWND hDlg — Windows-описатель окна диалога;
  • UINT message — код сообщения;
  • WPARAM wParam, LPARAM IParam — два параметра, сопровождающих сообщение.
  • Диалоговая процедура имеет характерную, давно устоявшуюся структуру. Первая ветвь switch-блока (WM_INITDIALOG) вызывается при открытии диалога, а вторая (WM_COMMAND) — при нажатии кнопок, расположенных в нем. Вместе с сообщением WM_COMMAND приходят два параметра, в которых запакована сопровождающая информация. В нашем случае это идентификатор (ШОК) кнопки ОК, так как другой традиционной кнопки Cancel (IDCANCEL) просто нет в шаблоне диалога. В Win32 идентификатор элемента управления спрятан в младших 16 битах wParam, и его приходится распаковывать. Функция EndDialog закрывает окно диалога.

    Оконная процедура

    Оконная процедура

    Теперь рассмотрим, как устроена оконная процедура wndProc. Ее имя уже дважды появлялось в тексте программы. Сначала был объявлен ее прототип, затем оно было присвоено одному из полей структуры типа WNDCLASSEX. Поле имеет тип указателя на функцию с особым прототипом оконной функции. Здесь полезно вспомнить, что имя функции трактуется компилятором C++ как ее адрес.
    Оконная процедура должна «просеивать» все посылаемые ей сообщения и обрабатывать те из них, которые были выбраны программистом для обеспечения желаемой функциональности. Типичной структурой оконной процедуры является switch-блок, каждая ветвь которого содержит обработку одного сообщения. В первом приближении наша оконная процедура реагирует только на три сообщения:
  • WM_COMMAND — о выборе пользователем одной из команд меню;
  • WM_PAINT — о необходимости перерисовать клиентскую область окна;
  • WM_DESTROY — о необходимости закрыть окно.
  • Сообщение WM_DESTROY (уничтожить окно) посылается системой уже после того, как окно исчезло с экрана. Мы реагируем на него вызовом функции PostQuitMessage, которая указывает системе, что поток приложения требует своего завершения, путем посылки сообщения WM_QUIT. Его параметром является код завершения, который мы указываем при вызове PostQuitMessage.
    Примечание
    Примечание

    Рассмотренная структура приложения Win32 позволяет сделать вывод, что в подавляющем числе случаев развитие приложения сосредоточено внутри оконной процедуры, а не в функции WinMain. Развитие приложения заключается в том, что в число обрабатываемых сообщений (messages) включаются новые. Для этого программист должен вставлять новые case-ветви в оператор switch (msg).
    Если оконная процедура не обрабатывает какое-либо сообщение, то управление передается в ветвь default. Вы видите, что в этой ветви мы вызываем функцию DefWindowProc, которая носит название оконной процедуры по умолчанию. Эта функция гарантирует, что все сообщения будут обработаны, то есть, удалены из очереди. Возвращаемое значение зависит от посланного сообщения.

    Вы, конечно, обратили внимание на обилие новых типов данных, которые используются в приложениях Win32. Многие из них имеют префикс Н, который является сокращением слова Handle — дескриптор, описатель. Описатели разных типов (HWND, HPEN, HBITMAP и т. д.) являются посредниками, которые помогают найти нужную структуру данных в виртуальном мире Windows. Объекты Windows или ее ресурсы, такие как окна, файлы, потоки, перья, кисти, области, представлены в системе структурами языка С, и адреса этих структур могут изменяться. В случае нехватки реальной памяти Windows выгружает из памяти ненужные в данный момент времени объекты и загружает на их место объекты, требуемые приложением. В системной области оперативной памяти Windows поддерживает таблицу, в которой хранятся физические адреса объектов. Для поиска объекта и управления им сначала следует получить у системы его описатель (место в таблице, индекс). Важно иметь в виду, что физический адрес объекта — понятие для Windows, а не для программиста. Описатель типа HANDLE можно уподобить номеру мобильного телефона, с помощью которого вы отыскиваете объект, перемещающийся в виртуальном мире Windows.

    Перья на основе растровых изображений

    Перья на основе растровых изображений

    Последним испытанием для геометрического пера будет произвольное bitmap-изображение. Если задать BS_PATTERN в качестве стиля кисти, на основе которой создается перо, то линия рисунка может иметь произвольный узор и толщину, что дает волю фантазии разработчика. Однако сначала следует создать ресурс bitmap-изображения, загрузить его и задать его описатель в поле IbHatch логической кисти. Для создания изображения:
  • Перейдите в окно Resource View.
  • Раскройте узел дерева под именем API.rc и убедитесь, что в дереве нет узла с именем Bitmap.
  • Вызовите контекстное меню на узле API.rc и дайте команду Add Resource.
  • В диалоге Add Resource выберите тип Bitmap и нажмите кнопку New.
  • Откройте окно Properties и в поле справа от текста ID задайте идентификатор IDB_PAT1 новому точечному изображению.
  • Измените размер изображения, уменьшив его до 5x5. Используйте для этой цели элементами управления (resize handles) рамки.
  • Создайте произвольное изображение с помощью контрастирующих цветов.
  • В окне Resource View вызовите контекстное меню на узле дерева IDВ_РАТ1 и выберите команду Insert Copy.
  • Измените язык копии на тот, который поддерживается системой, например English (United States), иначе не получится, и нажмите ОК.
  • Теперь вы имеете копию изображения с теми же размерами и идентификатором. Здесь удобно перевести окно Properties в режим Floating или сдвинуть его нижнюю границу вверх. Измените идентификатор нового изображения на юв_РАТ2 и, при желании, возвратите язык ресурса.
  • Измените узор второго изображения и повторите пункты 7-10, задав идентификатор ЮВ_РАТЗ для третьего изображения.
  • Возвратитесь в окно API.CPP и введите в число локальных переменных функции wndProc новые массивы:

    //===== Массив идентификаторов bitmap

    static 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_*. Вы, вероятно, захотите их исследовать самостоятельно. При этом можете использовать уже надоевшую, но достаточно эффективную схему, которой мы пользуемся сейчас.



    Программы управляемые событиями

    Программы, управляемые событиями

    В этом уроке мы с помощью 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, могут быть следующие:
  • каркас MFC-приложения, так или иначе, содержит внутри себя структуру традиционного Windows-приложения;
  • многие методы классов MFC содержат, инкапсулируют вызовы API-функций;
  • накоплен огромный банк готовых решений на основе SDK, которые достаточно просто внедряются в приложения на основе MFC, и не пользоваться которыми означает обеднять себя.
  • В состав API входят не только функции, более 2000, но и множество структур, более 800 сообщений, макросы и интерфейсы. Цель настоящей главы:

  • показать традиционную структуру Windows-приложения;
  • продемонстрировать способы управления таким инструментом подсистемы GDI (Graphics Driver Interface), как перо Windows.
  • Основной чертой всех Windows-приложений является то, что они поддерживают оконный интерфейс, используя при этом множество стандартных элементов управления (кнопки, переключатели, линейки, окна редактирования, списки и т. д.). Эти элементы поддерживаются с помощью динамических библиотек (DLL), которые являются частью операционной системы (ОС). Именно поэтому элементы доступны любым приложениям, и ваше первое приложение имеет почти такой же облик, как и любое другое. Принципиально важным отличием Windows-приложений от приложений DOS является то, что все они — программы, управляемые событиями (event-driven applications). Приложения DOS — программы с фиксированной последовательностью выполнения. Разработчик программы последовательность выполнения операторов, и система строго ее соблюдает. В случае программ, управляемых событиями, разработчик не может заранее предсказать последовательность вызовов функций и даже выполнения операторов своего приложения, так как эта последовательность определяется на этапе выполнения кода.

    Программы, управляемые событиями, обладают большей гибкостью в смысле выбора пользователем порядка выполнения операций. Характерно то, что последовательность действий часто определяется операционной системой и зависит от потока сообщений о событиях в системе. Большую часть времени приложение, управляемое событиями, находится в состоянии ожидания событий, точнее сообщений о них. Сообщения могут поступать от различных источников, но все они попадают в одну очередь системных сообщений. Только некоторые из них система передаст в очередь сообщений вашего приложения. В случае многопотокового приложения сообщение приходит активному потоку (thread) приложения. Приложение постоянно выполняет цикл ожидания сообщений. Как только придет адресованное ему сообщение, управление будет передано его окопной процедуре.

    Примечание
    Примечание

    Если вы хотите получить более полное представление о процессах и потоках в Windows, то обратитесь к последней главе этой книги, которая носит более познавательный, чем практический характер.
    Наступление события обозначается поступлением сообщения. Все сообщения 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.

    Прохождение сообщений в системе

    Прохождение сообщений в системе

    Рассмотрим ситуацию, когда пользователь приложения нажимает клавишу, а система вырабатывает сообщение об этом событии. Вы знаете, что Windows обеспечивает поддержку клавиатуры, не зависящую от типа устройства (device-independent support). Для каждого типа клавиатуры она устанавливает соответствующий драйвер, то есть специальную программу, которая служит посредником между клавиатурой и операционной системой. Клавиатурная поддержка Windows не зависит от языка общения с системой. Это достигается использованием специальной клавиатурной раскладки (layout), которую пользователь выбрал в данный момент. Каждой клавише на уровне аппаратуры присвоено уникальное значение — идентификатор клавиши, зависящий от типа устройства и называемый скан-кодом.
    Примечание
    Примечание

    На самом деле, когда пользователь вводит символ, то клавиатура генерирует два события и два скан-кода — один, когда он нажимает клавишу, и другой, когда отпускает. Скан-коды с клавиатуры поступают в клавиатурный драйвер, который, используя текущую раскладку, транслирует их и преобразовывает в сообщения.
    Клавиатурный драйвер интерпретирует скан-код и преобразует его в определяемый Windows код виртуальной клавиши (virtual-key code), не зависящий от типа устройства и идентифицирующий функциональный смысл клавиши. После этого преобразования скан-кода драйвер создает сообщение, в которое включает: скан-код, виртуальный код и другую сопутствующую информацию. Затем он помещает сообщение в специальную очередь системных сообщений.
    Windows выбирает сообщение из этой очереди и посылает в очередь сообщений соответствующего потока (thread). В конце концов, цикл выборки сообщений данного потока передает его соответствующей оконной процедуре для обработки. Модель ввода с клавиатуры в системе Windows представлена на Рисунок 3.1.



    Путь прохождения сообщений от клавиатуры

    Рисунок 3.1. Путь прохождения сообщений от клавиатуры

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

    Каждый обработчик события (драйвер) помещает сформированное сообщение в определенную динамическую структуру данных в памяти. Другие аппаратные и программные обработчики точно так же формируют свои сообщения, ставя их в очередь за уже существующими. Так формируется системная очередь сообщений.

    Операционная система постоянно работает с очередью и, анализируя текущее сообщение, решает, какому приложению следует его передать. Она переписывает его в другую структуру данных в памяти — очередь сообщений конкретного приложения. Приложение реагирует или не реагирует на сообщение, но в любом случае удаляет его из очереди сообщений. Здесь важно понять, что по отношению к приложению сообщения появляются случайным образом и невозможно предсказать, какое сообщение появится в следующий момент.



    Путь прохождения сообщений Windows

    Рисунок 3.2. Путь прохождения сообщений Windows

    Путь прохождения сообщений Windows

    Развитие начальной заготовки

    Развитие начальной заготовки

    Мы хотим показать способы развития начальной заготовки приложения Win32. В наши планы входит создание пера Windows и управление его стилями. Кроме того, мы хотим показать, как можно изменять цвет пера с помощью стандартного диалога Windows. Существует множество стандартных диалогов для управления разными объектами системы, например открытие файла, выбор шрифта, поиск и замена текста. Мы будем управлять стандартным диалогом по выбору цвета. Обычным приемом при работе с каким-либо из стандартных диалогов является использование подходящей вспомогательной структуры. Поля структуры помогают инициализировать элементы диалога при его открытии, а также извлечь результат после его завершения.
    В нашем случае диалог вызывается ЛР1-функцией chooseColor, которая требует задать в качестве параметра адрес структуры типа CHOOSECOLOR. Ее надо предварительно создать и использовать для хранения текущего цвета, а также цвета, выбранного пользователем в рамках диалога. Цвет должен храниться в поле rgbResult этой структуры. Вы помните, что оконная процедура многократно, при обработке каждого сообщения, получает и вновь отдает управление системе. Ее локальные (автоматические) переменные будут каждый раз создаваться и погибать. Следовательно, они не в состоянии запомнить текущий выбранный цвет. Выход — использовать либо глобальные, либо статические переменные. Используя второй способ, вставьте в начало тела функции WndProc следующие переменные:
    //===== Структура для работы со стандартным диалогом

    CHOOSECOLOR cc;

    //===== Переменная для хранения текущего цвета

    static 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. Наша оконная процедура уже реагирует на это сообщение, но пока еще не рисует. Теперь создадим команду меню, при выборе которой диалог должен появится на экране. Для этого:

  • Перейдите в окно Resource View.
  • Раскройте узел дерева ресурсов под именем Menu.
  • Выполните двойной щелчок на идентификаторе всей планки меню IDC_API.
  • В окне редактора меню переведите фокус ввода в окно на планке меню с надписью Type here (Внимание, там два таких окна!).
  • Введите имя меню Edit и переведите курсор вниз в пустое поле для команды.
  • Введите имя команды меню Color.
  • Откройте окно Properties и убедитесь, что команда получила идентификатор ID_EDIT_COLOR.
  • Перетащите мышью меню Edit на одну позицию влево.
  • Запустите приложение (Ctrl+F5) и опробуйте команду меню Edit > Color. Диалог имеет две страницы. Для того чтобы убедиться в правильном функционировании статического массива любимых цветов (custColors), раскройте вторую страницу, выберите несколько цветов в ее правой части, нажимая кнопку Add to Custom Colors. Обратите внимание на то, что выбранные цвета попадают в ячейки левой части диалога. Закройте и вновь откройте диалог. Новые цвета должны остаться на месте, так как они сохранились в массиве CustColors.

    Если вы знакомы со структурой

    Стартовая заготовка приложения Win32

    Если вы знакомы со структурой приложения Win32, то можете безболезненно пропустить несколько параграфов и перейти к параграфу с заголовком «Развитие начальной заготовки».

    Рассмотрим более подробно структуру традиционного Windows-приложения, представленную нам мастером Win32 Application Wizard Studio.Net. Программа спроектирована как шаблон (стартовая заготовка), который можно развивать, внося в него желаемую разработчиком функциональность.

    Создайте новый проект приложения Win32. Для этого:

  • В меню File > New выберите команду Project.
  • В появившемся диалоге New Project, в окне Project Type раскройте узел дерева под именем Visual C++ Projects и выберите узел Win32 Projects.
  • В окне Templates выберите тип проекта Win32 Project.
  • В окне Name задайте имя проекта: API. В окне Location задайте или оставьте без изменения местоположений новой папки с файлами решения (solution).
  • Нажмите ОК и проанализируйте предлагаемые по умолчанию мастером Win32 Application Wizard настройки проекта.
  • Нажмите кнопку Finish.
  • Запустите стартовую заготовку и убедитесь, что она создает окно с планкой меню и реагирует на shortcut-комбинацию Alt+? или Alt+/, создавая диалог About. Раскройте дерево в окне Class View студии и щелкните два раза имя глобальной переменной hlnst. Курсор переходит в окно редактора, где вы видите заготовку традиционного приложения Win32. Надо отметить, что она богаче оснащена, чем аналогичные заготовки предыдущих версий Visual C++. Кроме пяти функций здесь содержатся ресурсы (меню, диалог, значок, строки текста, и клавиатурный ускоритель). Вы можете убедиться в этом, раскрыв дерево ресурсов в окне Resource View, которое входит в блок страниц вместе с окном Class View. Анализ и развитие этой заготовки мы произведем немного позже, а сейчас приведем листинг, который создал мастер Win32 Application Wizard.1

    // API.cpp : Определяет точку входа приложения

    //

    #include "stdafx.h"

    #include "API.h"

    #define MAX_LOADSTRING 100

    //======== Глобальные переменные:

    HINSTANCE hlnst;

    // Текущий экземпляр

    TCHAR szTitle[MAX_LOADSTRING];

    // Текст заголовка окна

    TCHAR szWindowClass[MAX_LOADSTRING];

    // Текст регистрации

    //======== Прототипы функций, входящих в данный модуль

    ATOM MyRegisterClass(HINSTANCE hlnstance);

    BOOL Initlnstance(HINSTANCE, int);

    LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

    LRESULT CALLBACK About(HWND, UINT, WPARAM, LPARAM);

    int APIENTRY WinMain(HINSTANCE hlnstance,

    HINSTANCE hPrevInstance,

    LPSTR IpCmdLine,

    int nCmdShow)

    {

    //======= TODO: Помещайте код здесь

    MSG msg;

    HACCEL hAccelTable;

    //======= Инициализация глобальных строк текста

    LoadString(hlnstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING); LoadString(hlnstance, IDC_API, szWindowClass, MAX_LOADSTRING);

    //======= Вызов функции регистрации приложения

    MyRegisterClass(hlnstance);

    //======= Инициализация приложения:

    if (!Initlnstance (hlnstance, nCmdShow))

    {

    return FALSE;

    }

    //======= Загрузка клавиатурных ускорителей

    hAccelTable = LoadAccelerators (hlnstance, (LPCTSTR)IDC_API);

    //======= Цикл ожидания и обработки сообщений:

    while (GetMessage(&msg, NULL, 0, 0))

    if (!TranslateAccelerator(msg.hwnd, hAccelTable, Smsg))

    {

    TranslateMessage(Smsg);

    DispatchMessage(Srasg);

    }

    }

    return 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;

    }

    Теперь ответим на один из

    Рисунок 3.3. Стили пера в Win32

    Теперь ответим на один из Теперь ответим на один из вопросов, которые были заданы по ходу текста этой главы. Для того чтобы изменился цвет текста, надо вставить вызов API-функции SetTextColor. И сделать это надо в ветви обработки сообщения WM_PAINT перед вызовом функции TextOut.

    SetTextColor(hdc, color) ;

    Подведем итоги. Наш пример иллюстрирует характерные особенности строения приложения, управляемого событиями:

  • оно должно состоять как минимум из двух функций: winMain и регистрируемой оконной процедуры;
  • последняя имеет вид отдельных ветвей, обрабатывающих те команды пользователя или сообщения Windows, которые выбрал разработчик;
  • в более сложных случаях для обработки событий, конечно же, необходимо предусмотреть множество отдельных функций или модулей программы;
  • приложения Win32, разработанные по рассмотренной схеме, имеют то преимущество, что они не несут дополнительной нагрузки в виде скрытого от ваших глаз каркаса приложения, поэтому дают очень быстрый код;
  • недостатком приложения Win32 является то, что для их разработки необходимо ориентироваться в мире из тысяч API-функций, вспомогательных структур, макросов и интерфейсов.


  • Структура Windowsприложения

    Структура Windows-приложения

    Рассмотренная модель выработки и прохождения сообщений поможет вам понять структуру, принятую для всех Windows-приложений. Последние два блока в рассмотренной схеме (Рисунок 3.1) определяют особенности строения любого Windows-приложения. Простейшее из них должно состоять как минимум из двух функций:
  • функции winMain, с которой начинается выполнение программы и которая «закручивает» цикл ожидания сообщений (message pump);
  • оконной процедуры, которую вызывает система, направляя ей соответствующие сообщения.
  • Каждое приложение в системе, основанной на сообщениях, должно уметь получать и обрабатывать сообщения из своей очереди. Основу такого приложения в системе Windows представляет функция winMain, которая содержит стандартную последовательность действий. Однако обрабатывается большинство сообщений окном — объектом операционной системы Windows.
    Примечание
    Примечание

    C точки зрения пользователя, окно — это прямоугольная область экрана, соответствующая какому-то приложению или его части. Вы знаете, что приложение может управлять несколькими окнами, среди которых обычно выделяют одно главное окно-рамку (Frame Window). С точки зрения операционной системы, окно — это в большинстве случаев конечный пункт, которому направляются сообщения. С точки зрения программиста, окно —это объект, атрибуты которого (тип, размер, положение на экране, вид курсора, меню, зна-чек, заголовок) должны быть сначала сформированы, а затем зарегистрированы системой. Манипуляция окном осуществляется посредством специальной оконной функции, которая имеет вполне определенную, устоявшуюся структуру.
    Функция winMain выполняется первой в любом приложении. Ее имя зарезервировано операционной системой. Она в этом смысле является аналогом функции main, с которой начинается выполнение С-программы для DOS-платформы. Имя оконной процедуры произвольно и выбирается разработчиком. Система Windows регистрирует это имя, связывая его с приложением. Главной целью функции winMain является регистрация оконного класса, создание окна и запуск цикла ожидания сообщений.

    Традиционное Windowsприложение

    Традиционное Windows-приложение
  • Программы, управляемые событиями
  • Прохождение сообщений в системе
  • Структура Windows-приложения
  • Стартовая заготовка приложения Win32 и ее анализ
  • Оконная процедура
  • Меню и диалог
  • Развитие начальной заготовки
  • Управление пером Windows


  • Управление пером Windows

    Управление пером Windows

    Если вы хотите самостоятельно освоить какой-либо технологический прием или способ управления ресурсами, а так же инструментами Windows, то лучше всего обратиться к разделу Platform SDK документации (MSDN). В блоке страничных окон, которыми вы успешно пользуетесь, имеется страница Dynamic Help, которая помогает быстро отыскать необходимую информацию в море документации, сопровождающей Studio.Net. Предположим, вы хотите научиться создавать перо Windows и начать с получения справки. Надо открыть вкладку Dynamic Help и набрать в окне редактора текст, который, как вам кажется, имеет отношение к искомой теме, например Реn.
    Окно динамической справки следит за вашим вводом и пытается найти подходящий раздел в документации. В нашем случае вы должны увидеть пару тем, связанных с пером Windows. Открыв первую из них (Pen Class), вы убеждаетесь, что попали в раздел Visual Basic Help, то есть не туда. Второй попыткой может быть выбор строки СРеп или CreatePen. Теперь динамическая справка приводит вас ближе к цели. Если вы вспомните, что сейчас мы пользуемся функциями API, то выбор темы CreatePen будет точным.
    Примечание
    Примечание

    При работе с MSDN вы можете создать свое собственное подмножество документов и сократить количество тем, предлагаемых ядром MSDN.
    Внимательно прочтя всю страницу текста справки из раздела Platform SDK, вы поймете, что перо Windows — это достаточно сложный и гибкий инструмент рисования. Не пренебрегайте также гипертекстовыми ссылками внизу экрана под рубрикой See Also. Выберите там ссылку ExtCreatePen для одноименной функции, которую мы собираемся использовать. Правила игры с функцией ExtCreatePen не так просты, как хотелось бы, но они позволяют управлять атрибутами пера в широком диапазоне. Оказывается кроме «простых» перьев можно создавать перья на основе кисти.

    Cамоучитель по Visual Studio.Net

    Анализатор кодов ошибок

    Анализатор кодов ошибок
  • Приложение на основе диалога
  • Дизайн диалога
  • Управление окном Toolbox
  • Реакция окна на уведомляющие сообщения
  • Создание и связывание переменных
  • Вставка значка
  • Диалоговое окно About
  • Внесение логики разработчика
  • Собственные методы класса
  • Поиск в реестре
  • Синтаксический анализ файла


  • Диалог About

    Диалог About

    При нажатии кнопки в области картинок мы вызываем диалог About, от которого отказались при создании проекта. Цель такого поступка — самостоятельно создать диалог, поместить в него растровое изображение и ввести команду для его запуска в меню управления (Control menu) главного окна.
    Создайте новый диалог (Project > Add Resource > Dialog > New), удалите из его окна кнопку Cancel, разместите в нем Static Control, Group Box и Picture Control.
    Для картинки установите свойства: Type: Icon, ID: IDC_EYE, Image: IDI_EYELEFT. Обратите внимание на то, что свойство Image недоступно, пока вы не установите тип изображения Icon. Для окна диалога свойство ID задайте равным IDD_ABOUTBOX. В класс CLookDlg введите обработчик сообщения WM_SYSCOMMAND. Каркас приложения вызывает обработчик этого сообщения в те моменты, когда пользователь выбирает команды из меню управления или когда он пользуется кнопками свора-чивания окна:

    void 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 и временно исключить те фрагменты кода, которые еще не могут работать. Для задания нового идентификатора:

  • Вызовите контекстное меню в окне Resource View и выберите команду Resource Symbols.
  • В окне появившегося диалога нажмите кнопку New.
  • В окне Name: следующего диалога введите 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 должен работать.

    Примечание
    Примечание

    Если вы не знаете, что такое разделитель команд меню (Menu Separator), то закомментируйте строку, вставляющую его и, запустив приложение, сравните облик меню с тем, который был до этого.

    Дизайн диалога

    Дизайн диалога

    Развитие диалогового приложения обычно начинают с размещения элементов управления на поверхности его окна — шаблона диалога. Откройте шаблон в окне редактора и включите панель инструментов Dialog Editor, если она еще не включена.
    Примечание
    Примечание

    Список доступных панелей можно увидеть, вызвав контекстное меню над пустым местом планки меню. Кроме того, нам понадобится окно Toolbox. Если его нет или вы его закрыли, то дайте команду View > Toolbox. В окне Toolbox нажмите кнопку Dialog Editor, и тогда в нем появится достаточно длинный список элементов управления, которые можно размещать на форме диалога.
    Итак, вы вооружены и экипированы для того, чтобы создать лицо диалога. Вы будете выбирать элементы управления из окна Toolbox и размещать их в окне шаблона. В этот момент свойства текущего элемента отражены в окне Properties. Так как вам одновременно понадобятся окна Resource View, Properties и Look.re, то будет удобно, если вы переведете окно Properties в режим Floating, вынете его из блока страниц и переместите в более удобное место.

    Если вы вновь посмотрите на

    Вставка значка
    Если вы вновь посмотрите на окно диалога (Рисунок 4.1), то увидите справа два элемента типа Picture Control с идентификаторами IDC_RIGHT и IDC_LEFT. Эти элементы необходимо связать с растровыми изображениями значков (ресурсы типа Icon). Так как приложение выполняет функции браузера, то сюда я намереваюсь вставить изображения глаз, которые в принципе можно создать средствами графического редактора Studio.Net. Однако более простым выходом является использование изображений, которые были созданы мастерами своего дела и уже существуют в bmp-файлах. Достаточно много изображений входит в стандартную поставку студии. Они расположены в нескольких папках по адресу ...\Microsoft Visual Studio.Net\Common7\Graphics\icons. Вот алгоритм связывания элемента типа Picture Control с растровым изображением.

  • Дайте команду Project Add > Resource (или нажмите Ctrl+R).
  • В окне диалога Add Resource выберите тип ресурса Icon и нажмите кнопку New.
  • Откройте файл с каким-либо существующим изображением File > Open и т. д., но не забудьте изменить фильтр поиска на (*.*).
  • Скопируйте найденное и открытое в рамках студии изображение в буфер (Ctrl+C).
  • Перейдите в предыдущее окно с пустым изображением (Ctrl+F6) и вставьте его (Ctrl+V).
  • В окне Properties измените идентификатор на тот, который был задан в качестве свойства Image для элемента Picture Control. В нашем случае это IDI_EYERIGHT или IDI_EYELEFT.
  • Закройте ненужные окна.
  • Пользуясь этим алгоритмом, создайте две новые картинки и свяжите их с элементами IDC RIGHT И IDC LEFT.
    Примечание
    Примечание

    Если вы найдете изображение одного глаза (скажем, левого) и откроете его в рамках студии, то изображение можно скопировать в новый ресурс типа Icon и перевернуть (сделать глаз правым), дав команду Image > Flip Horizontal. Исследуйте и другие команды этого меню.
    Элементы управления типа Picture Control можно сделать «чувствительными». Покажем, как ввести в приложение способность реагировать на нажатие кнопки мыши в области, занимаемой нашими изображениями глаз. По схеме, которую вы использовали, когда вводили в класс диалога реакцию на WM_HSCROLL, создайте функцию — обработчик сообщения WM_LBUTTONDOWN (нажата левая кнопка мыши). В тело заготовки для функции-обработчика внесите следующий код:

    void CLookDlg::OnLButtonDown(UINT nFlags, CPoint point)
    {
    CRect left, right;

    //====== Узнаем координаты левой картинки

    GetDlgItem{IDC_LEFT)->GetWindowRect(Sleft);

    //====== Переход к относительным координатам

    ScreenToClient(Sleft);

    //====== Узнаем координаты правой картинки

    GetDlgItem(IDC_RIGHT)->GetWindowRect(Sright) ;
    ScreenToClient(bright);

    //====== Объединяем площади двух картинок

    left.UnionRect(left,right);

    //====== Если координаты курсора внутри этой площади

    if (left.PtlnRect(point))

    //======Вызываем диалог About

    OnSysCommand(IDM_ABOUTBOX,0);

    //====== Вызов родительской версии CDialog::OnLButtonDown(nFlags, point);

    }

    Окно диалога для поиска элементов управления

    Рисунок 4.1. Окно диалога для поиска элементов управления

    Окно диалога для поиска элементов управления Команда Rename > Tab из контекстного меню заголовка вставки позволяет переименовать всю вставку, а команда Rename > Item из контекстного меню самой вставки позволяет переименовать элемент. Команда Show > All > Tabs работает по принципу переключателя. Команда List > View, работающая по этому же принципу, позволяет переключать режим просмотра инструментов (значки/список). Команда Sort > Items > Alphabetically из контекстного меню заголовка вставки позволяет отсортировать инструменты, а команды Move > Up или Move > Down из меню окна — переместить их.

    Поиск в реестре

    Поиск в реестре

    Алгоритм поиска в реестре основан на предварительном анализе тех записей, которые внесла в него Studio.Net 7.0. Если параметры вашей установки отличаются от тех, которые использованы здесь, то вам надо провести анализ записей реестра по Ключу: HKEY_LOCAL_MACHINE\ SOFTWARE\ Microsoft\VisualStudio\7.0 И найти в них такое поле Value, которое содержит путь к папке со Studio.Net. Введите в класс еще одну функцию, которая пытается найти путь к файлу winError.h, используя записи в реестре Windows.

    string CLookDlg: string CLockDlg::GetPathFromRegistry(void)
    {

    HKEY hkey; // Описатель ключа реестра
    TCHAR path[MAX_PATH] ; // Временный буфер
    TCHAR vs[] =
    // Строка для поиска ключа
    //"SOFTWARE\\Microsoft\\VisualStudio\\7.0\\Setup\\VC";
    DWORD dw, d;
    //====== Пытаемся найти ключ и открыть его,

    //====== затем пробуем открыть второй ключ (subkey)

    //====== и прочесть его запись (value)

    bool 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 помогает управлять параметрами диалога по поиску папки.

    Примечание
    Примечание

    Для многих стандартных диалогов типично использование специальных структур, их обслуживающих. Вспомните диалог по выбору цвета. Там использовалась структура типа CHOOSECOLOR. Диалог по поиску и замене текста обслуживается структурой FINDREPLACE. Диалог по выбору шрифта работает со структурой CHOOSEFONT. Здесь мы используем структуру BROWSEINFO.

    Функция SHBrowseForFolder запускает диалог, который позволяет пользователю выбрать папку. Она возвращает адрес списка идентификаторов pidl (pointer to identifier list), описывающих местоположение выбранной папки по отношению к корню (root) пространства имен (namespace). По умолчанию namespace — это рабочий стол (desktop). При работе с элементами СОМ важно помнить, что после использования интерфейса мы обязаны освободить его ресурсы вызовом метода Free и скорректировать (уменьшить на единицу) счетчик числа его пользователей (Release). Функция SHGetPathFromlDList преобразует список идентификаторов в системный файловый путь (szDir), который мы копируем в строку path.

    Приложение на основе диалога

    Приложение на основе диалога
    Уверен, что многие из читателей, разрабатывая свой код или запустив чужой многообещающий код, получали сообщение типа:

    0xC000000S STATUS_ACCESS_VIOLATION

    Раньше, во времена DOS, довольно часто можно было видеть сообщение «General Protection Fault» («Общая ошибка защиты»). Сообщения такого рода иногда вызывают чувство беспомощности и досады. Первая мысль — случилось что-то ужасное, вторая — неужели нельзя поподробнее объяснить причину отказа? Теперь, во времена COM (Component Object Model — Модель многокомпонентных объектов), количество ошибок и различного рода несоответствий драматически возрастает. В файле WinError.h, который можно найти в папке Include Visual Studio 6, содержатся описания свыше 1300 кодов ошибок, а в 7-й версии этот же файл содержит описания уже свыше 2500 кодов. Не будет преувеличением сказать, что не каждый разработчик будет с энтузиазмом искать файл WinError.h, а затем искать в нем код своей очередной ошибки.
    Анализируя файл WinError.h, можно заметить, что есть две категории ошибок. Первая категория — коды \Win32-ошибок, которые имеют вид десятичного числа, например:
    #define ERROR_ACCESS_DENIED 5L

    Надеюсь, вы не забыли, что суффикс ' L' задает тип константы (long). Вторая категория — это коды ошибок, возвращаемых многими OLE- и СОМ-АР1-функци-ями, например:
    #define E_NOTIMPL _HRESULT_TYPEDEF_(0x80004001L)
    Последние имеют вид шестнадцатеричных чисел, которые хранятся в переменных типа HRESULT.

    Примечание
    Примечание

    Этот тип не является еще одним описателем (Handle), как может показаться, судя по его имени. Он определен в файле WTypes.h оператором эквивалентности typedef LONG HRESULT и используется как набор битовых полей, имеющих определенный смысл. Самым необычным в этой ситуации является то, что суть ошибки (ее текстовое описание), то есть то, что более всего интересует программиста, присутствуете файле WinError.h в виде комментария. Вот фрагмент файла, который описывает одну ошибку:

    // 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 Формат кода Win32-ошибок

    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
    Символы имеют следующий смысл:

  • с — Customer code flag (флаг пользователя);
  • R — Reserved (зарезервировано для будущего использования);
  • s — Severity (Успех или неудача);
  • Rl, Cl, N, г — зарезервированная часть кода Facility.
  • Два старших бита Win32-oum6oK кодируют такие категории степени ошибки:

  • 00 —Success (Успех);
  • 01 — Informational (Информационное сообщение);
  • 10 — Warning (Предупреждение);
  • 11 — Error (Отказ).
  • Зададимся целью разработать приложение, которое можно назвать анализатором кодов ошибок. С его помощью пользователь, зная код ошибки, сможет быстро получить всю информацию, которая хранится в файле WinError.h и связана именно с этой ошибкой. На примере разработки приложения мы продемонстрируем такие технологические приемы, как:

  • создание приложения на основе диалога;
  • работа с текстовыми строками и потоками ввода-вывода, определенными в STL (Standard Template Library);
  • использование стандартного диалога по поиску папки; О поиск в реестре Windows.
  • Основная идея приложения заключается в том, что при его открытии происходит попытка с помощью реестра найти файл 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 ERROR_VECTOR;
    Определения такого типа упрощают создание ссылок на контейнеры или на его составные части. Перед тем как мы приступим к разработке приложения, отметим, что MFC-приложения на основе диалога имеют некоторые преимущества перед другими типами приложений. Главным из них является простота структуры классов и возможность пользоваться глобальными функциями MFC для обмена данными между окнами и переменными диалогового класса. Надо признать, что Эти функции (типа DDX_— Dynamic Data Exchange И DDV_ — Dynamic Data Validation) очень удобны и надежны. Конечно, приложения рассматриваемого типа не обладают такими разнообразными возможностями, как приложения типа MDI (Multiple Document Interface), но для определенного класса задач они являются оптимальным выбором.
    Начнем с создания стартовой заготовки приложения, основанного на диалоге. Тип приложения, как вы помните, выбирается с помощью мастера MFC Application Wizard. В левой части своего окна он имеет список команд, которые играют роль вкладок, раскрывающих различные окна-страницы правой части окна, поэтому команды слева мы будем называть вкладками, а окна справа — страницами. Для создания заготовки:

  • В меню File t New выберите команду Project.
  • В появившемся диалоге New Project, в окне Project Type раскройте ветвь дерева под именем Visual C++ Projects и выберите ветвь Win32 Projects.
  • В окне Templates выберите тип проекта: Win32 Project.
  • В окне Name задайте имя проекта: Look. В окне Location задайте или оставьте без изменения местоположение новой папки с файлами решения (solution).
  • В окне мастера MFC Application Wizard выберите вкладку Application Type и в окне справа укажите тип MFC-приложения — Dialog based.
  • Выберите вкладку User Interface Features и введите заголовок окна диалога — Look for an Error Code, так как английский язык в ресурсах работает значительно надежней русского, по крайней мере в бета-версии Studio.Net 7,0. Снимите флажок About — это упростит стартовое приложение.
  • Перейдите на страницу Advanced Features и снимите флажок ActiveX Controls.
  • Проанализируйте содержимое страницы Generated Classes. Здесь вы можете изменить имена двух классов, которые создаст мастер, но лучше этого не делать, так как имена составляются по стандартной схеме, которая упрощает обмен идеями в сообществе разработчиков.
  • Нажмите кнопку Finish. После непродолжительной паузы вы должны увидеть окно Solution Explorer.
  • Примечание
    Примечание

    Если это (или какое-то другое) окно оказалось в режиме Auto Hide, а вам он не подходит, то надо сделать окно активным и вменю Window снять флажок с команды Auto Hide. Окно перейдет в режим Docable. Эти же действия надо проделать со всеми другими окнами студии, которые вы хотите поместить в блок страниц, открываемых вкладками. Любое окно можно также перевести в режим Floating и вытащить из блока страниц. Для того чтобы снова вставить его в блок, надо перевести его в режим Docable, «взять» за заголовок и указать его новое.место среди вкладок блока. Важно то, что указатель мыши должен находиться в этот момент в области ярлыков вкладок.

    Запустите стартовую заготовку и убедитесь, что она создает диалог со значком, двумя кнопками и текстом «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:
  • Перейдите в окно Resource View, раскройте дерево ресурсов и дважды щелкните на идентификаторе диалога IDD_LOOK_DIALOG.
  • Откройте окно Properties, в разделе Appearance найдите свойство MinimizeBox и измените его значение на TRUE.
  • В окне Resource View, раскройте узел Icon, выберите идентификатор значка IDR_ MAINFRAME и нажмите клавишу Delete.
  • В окне редактора кодов (LookDlg.cpp) целиком удалите тела двух функций OnPaint, OnQueryDraglcon, два элемента карты сообщений: ON_WM_PAINT и ON_WM_QUERYDRAGICON и строку вызова Loadlcon из тела конструктора класса.
  • В файле LookDlg.h удалите объявления этих функций и переменную HICON m_hlcon.
  • Теперь в тело функции 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
    Запустите приложение и убедитесь, что окно диалога теперь сворачивается и значок на месте.

    Реакция окна на уведомляющие сообщения

    Реакция окна на уведомляющие сообщения
    Наш анализатор кодов ошибок по сути является браузером (инструментом для просмотра) файла WinError.h с особой структурой. Мы хотим дать пользователю возможность выбрать один из двух вариантов просмотра:
  • последовательный, с помощью счетчика Spin Control или ползунка slider Control,
  • поиск по значению, задаваемому в поле элемента IDC_FIND типа Edit Control.
  • Отметим, что счетчик (spinner) раньше назывался Up-Down control, а ползунок (slider) — Trackbar Control. Знание этого факта помогает понять, почему сообщения (стили) счетчика содержат аббревиатуру UD, а ползунка — тв. Все три используемых элемента (счетчик, ползунок и поле редактирования IDC_FIND) должны быть синхронизированы так, чтобы изменение позиции счетчика отслеживалось ползунком, и наоборот. Ввод пользователем кола ошибки в поле редактирования IDC_FIND должен вызывать мгновенную реакцию приложения и отражаться в показаниях счётчика и ползунка, но только в случае, если требуемый код найден.

    Примечание
    Примечание

    Напомним, что элементы управления на форме диалога являются дочерними окнами (child-windows) по отношению к окну диалога. И этот тип отношений parent-child (родство) не является отражением наследования в смысле ООП. Он существовал до того, как была создана MFC. Важно то, что дочерние окна генерируют уведомляющие сообщения об изменениях, происходящих в них а система направляет их в оконную процедуру родительского окна.
    Здесь мы должны использовать способность родительских (parent) окон реагировать на уведомляющие сообщения, исходящие от их «детей». Сообщения такого типа можно условно поделить на старые (Windows 3.x) и новые (Win32). Старых много, так как каждый тип элементов имеет несколько уведомляющих сообщений. Посмотрите Help > Index, задав индекс EN_ (Edit Notifications), и вы увидите, что элемент типа окна редактирования уведомляет своего родителя о таких событиях, как: EN_CHANGE (изменился текст), EN_KILLFOCUS (окно теряет фокус ввода), EN_UPDATE (окно готово к перерисовке) и множестве других.

    Наряду со старыми существует одно новое универсальное событие WM_NOTIFY. Теперь при создании новых элементов управления не надо плодить сообщения типа WM_*, которых и так очень много. Все могут обойтись одним — WM_NOTIFY. Его универсальность состоит в том, что все новые типы элементов умеют генерировать это одно сообщение. В дополнение они сопровождают его указателем на структуру NMHDR (Notify Message Header), которая способна «привести» за собой различные другие структуры. Весь трюк состоит в том, что, получив это сообщение вместе с указателем NMHDR* pNMHDR, который на самом деле показывает на другую, более сложную структуру, класс родительского окна знает тип элемента и, следовательно, знает, к какому типу надо привести этот указатель. Например, при изменении показаний счетчика система посылает родительскому окну сообщение WM_NOTIFY, в IParam которого помещен указатель типа NMHDR*:

    typedef struct 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 реакции на уведомляющие сообщения:

  • Откройте шаблон диалога и установите курсор мыши на счетчике (IDC_SPIN).
  • В окне Properties нажмите кнопку с подсказкой ControlEvents.
  • В появившемся списке уведомляющих сообщений, которые генерирует счетчик, выберите UDN_DELTAPOS, а в ячейке справа укажите действие — .
  • Перейдите в окно 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 каждый раз, когда в нем происходят изменения. Это сообщение работает по старой схеме и не влечет за собой необходимости преобразовывать указатели на структуры данных.
  • Вновь откройте шаблон диалога и установите курсор мыши в окно IDC_FIND.
  • В окне Properties нажмите кнопку с подсказкой ControlEvents.
  • В появившемся списке уведомляющих сообщений, которые генерирует окно редактирования, выберите сообщение EN_CHANGE и его реализацию .
  • Проверьте результаты работы 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.

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

    Синтаксический анализ файла

    Синтаксический анализ файла

    До сих пор мы исходили из предпосылки, что вся информация об ошибках уже получена и хранится в контейнере структур ERROR_VECTOR m_vector. Контейнер заполняется в функции ReadErrors, которая в цикле вызывает функцию GetNextErrorCode. Пришла пора создать эту функцию. Мы решили сделать ее глобальной, так как ее задача — найти и расшифровать очередную ошибку в потоке типа if stream (то есть файле) — довольно универсальна и не зависит от данных оконного класса. Глобальную функцию нет необходимости объявлять в интерфейсе класса (header-файле), поэтому просто вставьте следующий код в файл LookDlg.cpp (после объявления массива gsFacilities):
    bool GetNextErrorCode(ifstreams is)
    {
    //===== Поиск и выбор очередной ошибки из потока is
    string s;

    //==== Ищем строку текста "Messageld:"
    int pos = FindText(is, s, "Messageld: ");
    //==== Если дошли до конца файла, уходим
    if (is.eof())

    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, требования к которой уже сформулированы. Она должна получать в параметрах:
  • ссылку на объект, управляющий потоком ввода, связанным с файлом;
  • ссылку на строку типа string, в которую помещается результат;
  • адрес строки, которую надо найти.
  • Если последний параметр задать по умолчанию, то функцию можно использовать в двух режимах. Вставьте тело этой глобальной функции в начало файла 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
    //=== Буферизованные потоки, связанные с файлами (STL)
    #include

    #include // Текстовые строки STL

    #include // Контейнеры STL типа vector

    //====== Работаем в пространстве имен std

    using namespace std;
    Запустите программу, устраните возможные синтаксические и семантические ошибки и хорошо протестируйте. Подведем итог:
  • мы узнали о двух форматах данных типа HRESULT, которые используются для хранения информации о результате выполнения каких-либо операций;
  • потренировались в использовании контейнера STL и некоторых из его алгоритмов;
  • научились работать с объектами класса string, определенного в STL;
  • узнали, как с помощью инструментов студии создаются диалог, переменные диалогового класса (связываемые с элементами управления диалога), а также функции обмена данными с этими полями;
  • познакомились с уведомляющими сообщениями, работающими по схеме WM_NOTIFY, принятой для многих новых элементов управления;
  • узнали, как можно синхронизировать работу нескольких элементов управления;
  • применили объекты классов потокового ввода-вывода для чтения файла с данными о кодах ошибок;
  • научились производить поиск необходимой информации в реестре Windows;
  • познакомились с простыми приемами синтаксического анализа текста с известной структурой.


  • Собственные методы класса

    Собственные методы класса
    Работая с классом, производным от класса MFC, разработчик не только вводит в него реакции на сообщения и переопределяет виртуальные функции. Он также вносит в класс свою собственную функциональность, вводя в него вспомогательные методы (helper functions). Сейчас мы создадим несколько таких функций. Новый метод ReadErrors будет заниматься поиском, чтением и анализом файла WinError.h.
  • Переведите фокус мыши на узел CLookDlg в дереве классов Class View, вызовите контекстное меню и дайте команду Add > Add Function.
  • В окне мастера Add Member Function Wizard заполните следующие поля:
  • Return type: bool,
  • Function name: ReadErrors.
  • В поле Access: задайте тип доступа — public.
  • В поле Comment: введите комментарий Search and read errors (моя версия Studio.Net не позволяет пользоваться русским языком в этом диалоге).
  • Просмотрите изменения в классе CLookDlg, которые произвел мастер. Комментарий он помещает в файл заголовков (интерфейс класса). Введите следующий код в тело новой функции:

    bool 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)) с дочерними окнами диалога и они «высветят» ошибку.
  • Переведите фокус мыши на узел CLookDlg в дереве классов Class View, вызовите контекстное меню и дайте команду Add > Add Function.
  • В окне мастера Add Member Function Wizard заполните следующие поля: Return type: void, Function name: Getlnfo, Parameter type: int, Parameter name: nPos.
  • Нажмите кнопку Add.
  • В поле Access: задайте тип доступа public:
  • 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.

    Создаем диалог

    Создаем диалог
    Важным моментом в этой процедуре является то, что каждый элемент управления должен быть идентифицирован в поле ID окна Properties. Другие свойства можно изменять в других полях этого окна, но большая часть из них уже имеет нужные значения. Только некоторые свойства следует изменить. Перечислим в табл. 4.3 (в порядке слева направо, сверху вниз) элементы управления и их идентификаторы.

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

    Тип элемента

    Заголовок (комментарий)

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

    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

    Проверьте особые свойства элементов, которые должны быть такими, как показано ниже. Если они другие, то введите коррективы:

  • IDC_SPIN — SetBuddylnteger: TRUE;
  • IDC_CURRENT — Readonly: TRUE;
  • IDC_SLIDER— AutoTicks: TRUE, Point: Top/Left, TickMarks: TRUE;
  • IDC_RIGHT — Image: IDI_EYERIGHT;
  • IDC_LEFT — Image: IDI_EYELEFT.


  • Создание и связывание переменных

    Создание и связывание переменных

    Итак, мы ввели в состав класса, управляющего главным окном приложения, способность реагировать на уведомляющие события UDN_DELTAPOS, EN_CHANGE, а также на событие Windows WM_HSCROLL. Теперь пора показать, как с помощью Studio.Net можно создать переменные, способные обмениваться данными с элементами управления (дочерними окнами) диалога. Технология обмена в Visual C++ давно устоялась и происходит по такому алгоритму:

  • При открытии окна диалога ему посылается сообщение WM_INITDIALOG, на которое откликается либо класс вашего диалога, либо родительский класс CDialog.
  • В функции отклика OnlnitDialog вашего класса вы делаете необходимые начальные установки и вызываете родительскую версию этой функции. Причем порядок этих действий зависит от типов элементов управления и их установок.
  • Родительская версия OnlnitDialog вызывает функцию UpdateData (FALSE) для провоцирования обмена данными между переменными диалогового класса и элементами управления. Направление обмена задается параметром функции UpdateData: если он TRUE, то обмен совершается в сторону переменных, а если FALSE, то в сторону элементов управления. Очевидно, что в состав класса надо ввести переменные и связать их с элементами управления.
  • После того или по мере того как пользователь ввел или вводит изменения в состояния элементов управления, вновь, если это предусмотрел программист, вызывается функция UpdateData (TRUE) или одна из функций типа GetDlgitem*. Они провоцируют обмен данными в сторону переменных.
  • Функция UpdateData вызывает функцию DoDataExchange, которая обязательно есть в каждом диалоговом классе. Последняя состоит из последовательности вызовов глобальных функций MFC типа DDX_ и DDV_, которые умеют надежно обменивать данные в обе стороны и вежливо сообщать о возможных ошибках и несоответствиях при обмене.
  • При закрытии диалога (здесь имеется в виду не наш диалог, который является главным окном приложения, а обычный модальный диалог) каркас приложения вновь вызывает UpdateData (TRUE) для того, чтобы произвести считывание данных, введенных пользователем.
  • Необходимо помнить, что простым элементам -управления (static Control, Button Control или Edit Control) обычно ставят в соответствие простые переменные типа int, BOOL или cstring. Более сложным элементам (Spin Control, Slider Control) обычно соответствуют переменные, которые являются объектами классов (CSpinButtonCtrl, CSliderCtrl). Сейчас мы введем в диалоговый класс переменные, которые мы свяжем (ассоциируем) с элементами управления. Некоторые из этих переменных мы уже заочно использовали в коде функций-обработчиков.

  • Откройте окно диалога, установите фокус на счетчик (IDC_SPIN) и вызовите контекстное меню.
  • В меню выберите команду Variable — появится мастер Add Variable Wizard.
  • В окне мастера установите флажок Control variable.
  • Переключатель Control-Value установите в положение Control.
  • В окне Control ID выберите идентификатор элемента IDC_SPIN.
  • В окне Variable Name задайте имя переменной m_ Spin.
  • В окне Access выберите тип доступа private.
  • В окне Comment задайте комментарий: Счетчик кодов.
  • Нажмите кнопку Finish.
  • В окне Class View отыщите новый узел Variables, раскройте его и щелкните два раза мышью элемент m_Spin. В окне LookDlg.h вы увидите, что мастер вставил декларацию:

    //====== Счетчик кодов

    CSpinButtonCtrl m_Spin;

    Найдите тело функции DoDataExchange и убедитесь, что в ней появилась строка:

    DDX_Control (pDX, IDC_SPIN, m__Spin) ;

    которая связывает элемент IDC_SPIN с объектом m_spin. Теперь повторите все действия для элемента IDC_SLIDER. В классе CLookDig должен появиться объект m_Slider класса CSliderCtrl.
    В окне диалога осталось еще довольно много элементов управления, с которыми не связаны никакие переменные. Сейчас мы создадим эти переменные, но предварительно напомним, что элементы типа static Control (поле текста) могут быть как управляемыми, так и нет. В последнем случае все они должны иметь один и тот же идентификатор IDC_STATIC. Мы будем управлять шестью элементами типа static и одним элементом (IDC_CURRENT) типа Edit Control. Все элементы будут связаны с переменными по схеме Value, то есть между ними будет происходить обмен с помощью функций DDX_Text, а переменные будут иметь тип cstring. Процедура создания и связывания переменных для всех элементов типа static одинакова, поэтому мы приведем описание только одной, а вы повторите ее для всех других.
  • Откройте окно диалога, установите курсор в окно IDC_TOTAL и вызовите контекстное меню.
  • В меню выберите команду Variable — появится мастер Add Variable Wizard.
  • В окне мастера Control ID: должен быть выбран идентификатор IDC_TOTAL.
  • Установите флажок Control variable.
  • Переключатель Control-Value установите в положение Value.
  • В окне Access: выберите тип доступа private.
  • В окне Variable Type: задайте тип переменной CString.
  • В окне Variable Name: задайте имя переменной m_Total.
  • Нажмите кнопку Finish.
  • Полезно просмотреть состав класса CLookDlg и убедиться в том, что в нем появилась новая переменная m_Total, а в тело DoDataExchange добавлена строка:

    DDX_Text(pDX, IDCJTOTAL, mJTotal);
    Вызов функции DDX_Text гарантирует, что в ключевые моменты жизни приложения будет производиться обмен между переменной m_Total и полем текста IDC_TOTAL. Вы никогда не будете явно вызывать функцию DoDataExchange. Ее вызывает функция UpdateData. Она создает объект вспомогательного класса CDataExchange и задает направление обмена. Если параметр функции UpdateData равен TRUE или отсутствует, то обмен осуществляется в сторону переменных, если он равен FALSE, то — наоборот. Каркас приложения без вашего участия и в нужные моменты вызывает UpdateData, но вы можете и сами вызывать эту функцию тогда, когда необходимо произвести обмен. Обычно это моменты, когда вам надо считать все данные из окон или, наоборот, отразить в окнах изменения в данных, произведенные программой. Сейчас повторите шаги по созданию переменных, связанных с окнами элементов типа static Control. Все переменные должны иметь тип cstring. Данные для этой операции приведены в табл. 4.4.

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

    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.

    Управление окном Toolbox

    Управление окном Toolbox

    При работе с окном диалога вам приходится пользоваться инструментальным окном Toolbox, которое имеет вкладки (tab) несколько необычного вида. Все доступные вкладки инструментов вы сможете увидеть, если воспользуетесь контекстным меню окна Toolbox и выберете команду Show > All > Tabs. Нажимая на заголовки вкладок, вы увидите, что пространство между ними заполняется списками тех элементов управления, которые соответствуют выбранной вкладке. Большинство из этих элементов пока недоступны, но они станут доступны в определенных ситуациях. Сейчас мы рассмотрим, как можно управлять вкладками и их содержимым. Вы можете:
  • добавить или удалить свою собственную вкладку;
  • добавить элементы в пространство любой вкладки или удалить их из него;
  • переименовать как саму вкладку, так и ее элементы;
  • временно спрятать ненужную вкладку и показать ее, когда это необходимо;
  • управлять обликом элементов на вкладке (значки или список);
  • перемещать элементы в пределах вкладки;
  • сортировать элементы вкладки по алфавиту.
  • Среди всех вкладок инструментов (tools) есть две особые, которые носят названия General и Clipboard Ring. Они всегда демонстрируются по умолчанию. Другие же вкладки появляются и исчезают в зависимости от контекста, то есть от типа редактора, с которым вы в данный момент работаете. Если выбрать какой-либо элемент на какой-либо вкладке и нажать клавишную комбинацию Shift+Ctrl+C, то этот элемент попадает на вкладку кольцевого буфера инструментов (Clipboard Ring). Нажатие Shift+Ctrl+V восстанавливает в окне текущей вкладки последний элемент из буфера. Повторное нажатие восстанавливает следующий элемент из кольцевого буфера. При многократном повторении Shift+Ctrl+V элементы кольцевого буфера циклически повторяются. Так написано в документации, но в бета-версии такого поведения не наблюдается.
    Для того чтобы добавить новую вкладку в окно Toolbox, надо щелкнуть правой клавишей мыши в окне и выбрать команду Add > Tab. Затем ввести имя вкладки во временном текстовом окне и нажать ENTER. Такие вкладки рекомендуют использовать для создания своего любимого набора инструментов. Один инструмент — стрелка выбора (Pointer) по умолчанию присутствует на всех вкладках. Для удаления вкладки надо выбрать команду Delete » Tab в контекстном меню, вызванном на заголовке вкладки. В документации эта команда называется Remove > Tab, а в действительности (в бета-версии) — Delete > Tab.
    Для того чтобы поместить на вкладку какой-либо элемент, надо выбрать в контекстном меню команду Customize > Toolbox и выбрать в окне диалога из двух списков (Рисунок 4.1) нужный элемент, включив флажок выбора слева от элемента, и нажать кнопку ОК. С помощью подобных же действий, но выключая флажок, можно убрать элемент из окна инструментов. Кнопка Browse диалога Customize > Toolbox позволяет найти элемент, который не обозначен в списке. Но помните, что многие СОМ-объекты не будут работать в проектах Studio.Net, если они не зарегистрированы, то есть не отражены в окнах диалога.



    Внесение логики разработчика

    Внесение логики разработчика

    Итак, мы покончили с интерфейсной шелухой и нам осталась самая интересная и трудная часть работы — внесение в приложение той логики, которая была определена на этапе постановки задачи. Она состоит в следующем. Мы должны найти файл Win Error, h, просканировать его и выудить из него полезную информацию. Эту информацию надо поместить в контейнер объектов типа ErrorType. Если вы помните, мы поместили объявление этого типа в начало файла LookDlg.h. Там же находится определение нового типа ERROR_VECTOR — контейнера структур ErrorType. Теперь настала пора ввести в класс главного окна сам контейнер и его размерность. Довольно часто в целях экономии времени переменные вводят в состав класса вручную, то есть без помощи инструментов Studio.Net. Сейчас мы так и поступим. В секцию private класса CLookDlg введите следующую декларацию:
    //====== Контейнер структур типа ErrorType

    ERROR_VECTOR m_Vector;
    //====== Размерность контейнера

    int m_nltems;


    Cамоучитель по Visual Studio.Net

    Документ и его представления

    Документ и его представления
    Библиотека MFC предоставляет 3 различных способа создания окон-рамок, обрамляющих представления. Два явных способа: вызов методов CFrameWnd:: Create или CFrameWnd:: LoadFrame и один неявный — путем создания шаблона документа. Класс MFC-приложения в рамках многодокументного интерфейса (MDI) поддерживает динамический список шаблонов документов, то есть объектов класса CMultiDocTemplate. Каждый из них управляет оркестром из четырех музыкантов: ресурсы, документ, связаное с ним представление и обрамляющее его окно-рамка. Этот квартет составляется в момент создания нового объекта CMultiDocTemplate внутри метода initinstance класса приложения, в нашем случае СТгееАрр. Найдите это место в программе и убедитесь, что параметрами конструктора являются перечисленные музыканты:

    CMultiDocTemplate* pDocTemplate;

    //====== Создание шаблона документов

    pDocTemplate = new CMultiDocTemplate(IDR_TreeTYPE,

    RUNTIME_CLASS(CTreeDoc), // документ

    RUNTIME_CLASS(CTreeFrame), // окно-рамка

    RUNTIME CLASS(CLeftView)); // представление

    //====== Вставка адреса шаблона в динамический список

    AddDocTemplate(pDocTemplate);

    Мы можем при желании поменять состав квартета, но должны оставить неизменным количество его исполнителей. В наши цели входит создание еще одной новой комбинации. Тот же документ будет связан с другими ресурсами I DR_DrawTYPE, другим представлением (CDrawView) и другой рамкой (CDrawFrame). Так как мы хотим управлять обоими квартетами, то удобно запомнить в классе приложения их адреса и пользоваться ими при необходимости создать новое или активизировать существующее окно MDI-документа. Введите в состав класса CTreeApp две public-переменные, которые будут хранить эти адреса. С учетом незначительных сокращений интерфейс класса должен иметь такой вид:

    class CTreeApp : public CWinApp

    {

    public:

    //====== Два шаблона документов

    CMultiDocTemplate *m_pTemplDraw; CMultiDocTemplate *m_pTemplTree;

    CTreeApp () ;

    virtual BOOL Initlnstance();
    afx_msg void OnAppAbout();
    DECLARE_MESSAGE_MAP()
    };

    Задание координат полигонов является утомительным

    Вспомогательные функции
    Задание координат полигонов является утомительным занятием, поэтому мы, учитывая учебный характер приложения, создали три вспомогательные функции, которые позволяют быстро воспроизвести три различных полигона: звезду, треугольник и пятиугольник. Далее нам необходим немодальный диалог, с помощью которого пользователь сможет создать произвольное количество новых полигонов, выбирая их типы с помощью нестандартных кнопок и управляя атрибутами полигонов (цветом фона, цветом и толщиной пера) с помощью синхронизированных между собой элементов управления. Дополните файл ТгееОос.срр кодами еще трех функций:
    void 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);

    Класс CPolygon

    Класс CPolygon
    В соответствии с архитектурой «документ — представление» мы должны ввести в класс документа некоторые новые структуры данных для хранения информации о файлах документов, обнаруженных в выбранной пайке или логическом диске. Файловые пути хранятся в контейнере текстовых строк типа vector. Пришлось отказаться от использования класса string из библиотеки STL, так как многие используемые нами методы классов и API-функции требуют в качестве параметров переменные типа CString из библиотеки MFC. Преобразование типов из CString в string и обратно потребует дополнительных усилий, поэтому проще взять CString в качестве аргумента шаблона vector. Для изображения мини-чертежей найденных документов в правом представлении (CRightview) расщепленного окна (CTreeFrame) удобно ввести в рассмотрение класс CDPoint и тип данных VECPTS:

    typedef vector > VECPTS;

    Эти типы данных мы разработали во втором уроке для обозначения множества реальных (World) координат точек изображаемых объектов. Перенесите указанные объявления из проекта My (см. урок 2) и вставьте их в начало файла TreeDoc.h до объявления класса CTreeDoc, но после директивы #pragma once. Вставляя объявление новых классов в тот же файл, мы экономим свои силы в процессе отладки приложения, потому что нам не надо так часто переключать окна и заботиться о видимости новых типов данных. Однако довольно часто при этом становятся невидимыми для новых классов старые типы, которые декларированы в этом же файле, но чуть ниже. Такие проблемы легко решаются с помощью упреждающих объявлений класса. Вставьте сразу за директивой #pragma once такое объявление:

    class CTreeDoc; // Упреждающее объявление

    В конец файла StdAfx.h вставьте строки, которые обеспечивают видимость некоторых ресурсов библиотеки STL:

    #include using namespace std;

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

    Каждый объект класса CPolygon должен иметь связь с данными документа. Это осуществляется путем запоминания адреса документа в переменной m_pDoc, которая играет роль обратного указателя. Такой прием, когда вложенный объект помнит адрес объемлющей его структуры данных, очень распространен в объектно-ориентированном программировании. Он существенно упрощает обмен данными между двумя объектами.

    Примечание
    Примечание

    Здесь трудно обойтись без специального метода установки обратного указа-теля, в нашем случае метода Set. Дело в том, что при создании документа надо сначала создать вложенные в него объекты других классов (вспомните правило: «C++ уважает гостей»). Но в этот момент им нельзя передать адрес документа, так как он еще не создан. В таких случаях поступают следующим образом. В заголовке конструктора документа создают пустые объекты (вызывают default-конструкторы вложенных объектов), а затем в теле конструктора документа, когда он уже существует, для вложенных объектов вызывают метод, устанавливающий обратный указатель. При этом объекту передают указатель на документ (на объект собственного класса). Например: m_Poly.Set(this);
    Обилие методов класса CPolygon сделано «на вырост». Сейчас каждый документ для простоты представлен одним полигоном. Реальные конструкции можно задать в виде множества полигонов. При этом каждый из них должен знать свои габариты. Метод GetRect позволяет вычислять и корректировать габариты полигона. Если вы будете применять эти идеи в более сложном проекте, то вам понадобится множество других методов. Например, методы, определяющие факт самопересечения полигона или взаимного их пересечения.

    Главными методами, которые реализуют концепцию архитектуры «документ — представление», являются Serialize и Draw. Метод Serialize позволяет общаться с файлами. Его особенность состоит в том, что он позволяет как записывать все данные объекта в файл, точнее в архив, так и читать их из файла. Здесь опять проявятся преимущества наследования от cobject, так как объекты классов, имеющих такого авторитетного родителя, обычно сами умеют себя сериализовывать.

    Примечание
    Примечание

    Термин «сериализация» приходится брать на вооружение, так как он довольно емкий, и чтобы его заменить, надо произнести довольно много слов о последовательном (in series) помещении данных объекта в архив, который связан с файлом. Кроме того, надо сказать о том, что в классе CArchive переопределены операции « и ». Просмотрите почти пустое тело функции Serialize в классе документа. Оно, тем не менее, намекает нам, как разделяются две разновидности общения с архивом. Вызов функции CArchive::IsStoring() возвращает ненулевое значение в случае, если архив используется для записи данных.

    Новый класс 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; // Пока не знаем обратного адреса

    MakeStar(); // Зададим полигон в виде звезды
    }

    Класс для нового представления документа

    Класс для нового представления документа

    При создании второго шаблона документов мы определили новую комбинацию классов, которые будут поддерживать функционирование окон нового типа. Клиентской областью этих окон будет управлять класс CDrawView, к созданию которого мы и приступаем.

  • Поставьте фокус на элемент Tree дерева классов в окне Class View, вызовите контекстное меню и выберите команду Add > Add Class.
  • В окне появившегося диалога выберите категорию MFC, шаблон MFC Class и нажмите кнопку Open.
  • В окне MFC Class Wizard задайте имя класса CDrawView и выберите класс CView в списке Base Class.
  • Нажмите кнопку Finish.
  • Примечание
    Примечание

    Среди классов списка Base Class теперь есть класс CObject. Как ни странно, но в Visual Studio 6 вы не могли бы рассчитывать на помощь ClassWizard при создании класса, производного от CObject.

    В папке проекта появились еще два файла (DrawView.h и DrawView.cpp), которые были автоматически включены в состав проекта. Класс приложения должен их видеть, поэтому вставьте в конец списка директив #include файла Тгее.срр еще одну:
    #include "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(m_pDocument);

    }

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

    Класс для просмотра изображений

    Класс для просмотра изображений

    Класс представления документа CRightView служит для иллюстрации содержимого всех документов, обнаруженных в текущей выбранной папке. В окне CRightView мы рядами и столбцами разместим другие простые окна, управляемые классом CWndGeom, которые будут иметь одинаковый размер и изображать геометрию конструкции, соответствующей данным документа. Причем изображение в контексте окна воспроизведут сами документы, точнее объекты m_poly, которые есть в каждом из них. Далее окна класса CWndGeom мы будем называть картинками.
    Так как количество документов в текущей папке произвольно и заранее не известно (но они все должны быть доступны пользователю), то, чтобы разместить все картинки, размеры окна CRightView должны быть переменными. Окно должно быть «резиновым». Класс CRightView был изначально создан мастером AppWizard как класс, способный прокручивать содержимое своего окна, так как в качестве базового класса для него был выбран csroliview. Благодаря этому класс приобрел способность следить за размерами своего окна и при необходимости создавать полосы горизонтальной и вертикальной прокрутки. Наша цель — научиться программно управлять размерами окна прокрутки, динамически создавать и уничтожать окна картинок и правильно изображать в них геометрию конструкции, опираясь на данные документа. Скорректируйте коды стартовой заготовки с интерфейсом класса так, как показано ниже:

    #pragma once

    //====== Класс для демонстрации содержимого документов

    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 m_pWnds;

    CTreeDoc* GetDocument()

    {

    return dynamic_cast (m_pDocument) ;

    }

    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) уже должен быть известен системе.

    Примечание
    Примечание

    Прекрасное решение дал Brad Pirtle, и вы можете найти его в одном из разде-лов CodeGuru, включив поиск по имени. Он создал свой класс CZoomView (производный от CScrolLView), в котором заменил функцию SetScrollSizes на другую — SetZoomSizes, а также переопределил (overrode) виртуальную функцию OnPrepareDC, родительская версия которой обнаруживает и запрещает попытку использовать формульные режимы. В своей версии OnPrepareDC он обходит вызов родительской версии, то есть версии CSrollView, и вместо этого вызывает «дедушкину» версию CView::OnPrepareDC, которая терпимо относится к формульным режимам. Этот пример, на мой взгляд, очень убедительно демонстрирует гибкость объектно-ориентированного подхода при разработке достаточно сложных приложений.

    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, которая должна:
  • Пройти по всему перечню объектов m_shapes класса CPolygon.
  • Вычислить исходя из текущего размера окна количество рядов и колонок мини-окон с изображениями полигонов.
  • Создать для каждого из них окно, управляемое классом CWndGeom.
  • Дальше события развиваются автоматически. После создания окна 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 () ;
    }

    Конструкторы и операции

    Конструкторы и операции

    Важными моментами в жизни объектов являются те, когда они копируются или создаются на основе уже существующих. Реализация конструктора копирования объектов просто обязательна, если вы пользуетесь контейнером объектов. В случае отсутствия или некорректного тела конструктора контейнеры откажутся работать с объектами класса. Обычным приемом при этом является реализация в классе операции присвоения operator= () и последующее ее воспроизведение в конструкторе копирования. Обратите внимание на тип возвращаемого значения операции присвоения. Это ссылка (CPolygon&) на активный или стоящий в левой части операции присвоения — *this, объект класса:

    CPolygoni CPolygon::operator=(const 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 m_Points.push_back(poly.m_Points[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(m_pDocument);
    }

    Замены такого рода, когда в h-файл вставляется код, а не только декларации, сопряжены с некоторыми неожиданными сообщениями со стороны компилятора. Здесь важно проявить терпение и не опускать руки раньше времени. Если вы правильно сделали замены, то после компиляции проекта получите предупреждение и сообщение об ошибке. С предупреждением справиться просто, если посмотреть справку по его коду (С4541). Выяснится, что для использования информации о типе указателей на этапе выполнения (run-time type information, которой пользуется выражение dynamic_cast(expression)), необходимо предварительно сделать установку специального режима компиляции. В Studio.Net это делается так:

  • Поставьте фокус в узел Tree окна Class View или окна Solution Explorer и дайте команду View > Property Pages (Alt+Enter).
  • В появившемся диалоге Property Pages раскройте узел дерева C/C++ и выберите элемент Language.
  • В таблице окна справа найдите свойство Enable Runtime Type Info и задайте для него значение Yes (/GR).
  • Аббревиатура /GR соответствует опции, задаваемой в командной строке компилятора. После повторной компиляции предупреждения исчезнут, однако ошибка останется. В такие моменты важно обратить внимание на имя файла, при компиляции которого была обнаружена ошибка. В нашем случае — это TreeFrm.cpp. Раскройте этот файл и просмотрите его начало, где стоят директивы #include. Сбой произошел в месте включения файла #include "Lef tview.h". Именно в него мы вставили новое тело функции GetDocument. Компилятор сообщает, что при анализе строки
    return dynamic_cast>(m_pDocument);
    он обнаружил неверный тип для преобразования (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(m_pDocument);
    }

    //====== Выбор системных значков

    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».

    Примечание
    Примечание

    C помощью функций SetWindowLong и SetWindowLongPtr можно перемещать окна вверх или вниз внутри иерархии окон, определяемой отношением, которое называется Z-order. Дело в том, что окна на экране упорядочены в соответствии с Z-order. Считается, что ось Z направлена на нас. Z-order служит механизмом, определяющим, видимо ли окно в данный момент или скрыто другими окнами, которые располагаются выше в иерархии Z-order. Вы можете программно изменять этот порядок.

    Немодальный диалог

    Немодальный диалог

    В предыдущем разделе мы научились редактировать данные документа, воздействуя мышью непосредственно на их представление, то есть облик документа, на экране монитора. Это довольно грубый, но быстрый и эффективный способ, позволяющий получить заготовку некоторой геометрии конструкции, которую впоследствии можно довести до желаемого состояния с помощью таблиц (элементов управления типа grid) или обычных окон редактирования. В практике проектирования геометрии устройств или описания геометрии расчетной области часто используют некоторые стандартные заготовки, которые служат отправной точкой для дальнейшей детализации и усложнения геометрии. Такие заготовки целесообразно выбирать с помощью окон диалога, работающих в немодальном режиме и зачастую называемых Toolbox-window. В них пользователь может выбрать одну из стандартных заготовок геометрии устройства или изменить атрибуты текущей. Создайте с помощью редактора диалогов Studio.Net форму диалога, которая выглядит так, как показано на Рисунок 5.5. Типы элементов управления, размещенных в окне диалога, и их идентификаторы сведены в табл. 5.1.



    Немодальный режим работы

    Немодальный режим работы

    Особенность работы с немодальным диалогом заключается в том, что надо затратить дополнительные усилия для корректного завершения его работы. Чтобы закрыть немодальный диалог, документация требует переопределить две виртуальные функции в классе диалога: обработчик закрытия диалога OnCancel и функцию PostNcDestroy. Существуют подробные рекомендации для завершения немодального диалога. Внутри вашей версии виртуальной функции OnCancel следует уничтожить окно диалога (DestroyWindow), а внутри другой виртуальной функции PostNcDestroy рекомендуется уничтожать объект диалогового класса (delete this;). С немодальным диалогом принято работать динамически (on heap frame), а не статически (on stack frame), как с модальным. Когда уничтожается окно Windows, то последним сообщением, которое ему посылается, является WM_NCDESTROY. Обработчик по умолчанию, то есть родительская версия cwnd: :OnNcDestroy, открепляет (detach) описатель окна HWND от объекта класса C++ и вызывает виртуальную функцию PostNcDestroy.
    Некоторые классы переопределяют ее для того, чтобы произвести освобождение своих объектов в области heap (динамическая память). При неудаче в процессе создания окна происходит вызов функции cwnd::PostNcDestroy, которая ничего не делает, но дает возможность виртуальным двойникам корректно освободить динамическую память. Мы тоже будем работать с диалогом динамически (on heap frame). В классе документа будет храниться адрес объекта с Pol у Dig, который должен корректно следить за стадиями создания и уничтожения диалога. Я намеренно не переопределял PostNCDestroy, чтобы показать альтернативный способ, допустимый в частных случаях. Так как наш диалог завершается командой Close с идентификатором IDOK, то, введя обработчик этой команды, мы можем с помощью родительской версии уничтожить Windows-окно, а затем освободить память, занимаемую объектом собственного класса:
    void 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();
    }



    Нестандартные кнопки

    Нестандартные кнопки
    Кнопкам управления, которые обычно размещаются в окне диалога, тоже можно придать нестандартный облик, пометив их bitmap-изображениями вместо традиционного текста. Для этой цели в библиотеке MFC имеется специальный класс CBitmapButton, объекту которого можно приписать до четырех изображений, соответствующих различным состояниям кнопки. Кнопка может быть в одном из следующих состояний:

  • нормальное (Up) — кнопка не нажата;
  • выбранное (Down) — кнопка не нажата;
  • в фокусе (Focused) — системный фокус расположен на кнопке;
  • недействующее (Disabled) — кнопка недоступна для пользователя.
  • Достаточно создать одно изображение кнопки, соответствующее первому состоянию, чтобы она функционировала. Размеры bitmap-изображений могут быть любыми, но важно, чтобы они были одинаковы. Система задает такой размер кнопке, какой имеет ее изображение в нормальном (первом) состоянии. При создании bitmap-ресурсов им следует придать идентификаторы в соответствии со следующими правилами:

  • Кнопке с заголовком, например ОК, имеющей 4 состояния, должны соответствовать 4 изображения с идентификаторами: "OKU", "OKD", "OKF", "OKX". Окончания U, D, F, X кодируют состояния: Up, Down, Focused, Disabled соответственно.
  • Идентификаторы изображений обязательно должны быть "строкового" типа, поэтому при их задании не забывайте вводить двойные кавычки.
  • Чтобы ассоциировать обычную кнопку в ресурсе диалога с этими изображениями, ей следует присвоить заголовок (caption) OK и выбрать стиль Owner draw. Это заставляет Windows посылать сообщения WM_MEASUREITEM и WM_DRAWITEM, которые обрабатывает каркас приложения, управляя обликом кнопок.
  • В классе диалога следует завести объект класса CBitmapButton и при инициализации диалога послать ему сообщение Autoload.
  • Заметьте, что выбор изображения происходит, опираясь на заголовок кнопки, а не на его идентификатор. Применим эту технологию для трех наших кнопок с заголовками TRI, PENT и STAR и придадим им нестандартный облик. Для этого:

  • В окне ResourceView вызовите контекстное меню на элементе Тгее.гс и выберите команду Add > Resource.
  • В появившемся диалоге выберите тип ресурса Bitmap и нажмите кнопку New.
  • В окне Properties или с помощью мыши задайте желаемый размер будущей кнопки (например, 40 на 25) и создайте изображение треугольника.
  • Присвойте новому ресурс идентификатор "TRIU".
  • Создайте копию изображения, временно заменив язык, и присвойте копии идентификатор "TRID", возвратив язык.
  • Повторите эти шаги для двух других кнопок, используя строковые идентификаторы: "PENTU", "PENTD", "STARU", "STARD".
  • С помощью Studio.Net введите (переопределите) в классе CPolyDlg виртуальную функцию OnlnitDialog и измените тело этой функции так, чтобы оно было:
  • BOOL CPolyDlg::OnInitDialog()
    {

    //====== Загрузка из ресурсов изображений кнопок

    m_cTri.AutoLoad (IDCJTRI, this);

    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.

    Обработка сообщений от элементов управления

    Обработка сообщений от элементов управления

    В окно диалога мы ввели четыре кнопки, при нажатии которых в класс диалогового окна посылается уведомляющее сообщение BN_CLICKED. При изменении данных в окнах редактирования посылаются другие сообщения EN_CHANGE. При воздействии на ползунки также посылаются уведомляющие сообщения, которые мы рассматривали в предыдущей главе. Однако, как было отмечено, ползунки посылают и обычные сообщения (WM_HSCROLL или WM_VSCROLL). Если в окне диалога имеется более одного ползунка, то сообщения от них удобно обработать в одной функции, которая вызывается в ответ на сообщение о прокрутке. Введите в класс CPolyDlg реакцию на WM_HSCROLL, так как наши ползунки ориентированы горизонтально:

    void 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 вставляет узел в дерево. Его параметры задают:

  • местоположение узла, то есть описатель родительского узла (h), О соответствующий узлу дерева текст (s),
  • индексы двух изображений (id, idSel) в уже сформированном списке типа CImageList.
  • Вставляемый в дерево логический диск надо проверить на наличие вложенных сущностей и вставить внутрь данного узла дерева хотя бы один элемент, когда диск не пуст. Если этого не сделаеть, то в дереве не будет присутствовать маркер (+), с помощью которого пользователь раскрывает узел.

    При проверке диска (функция NotEmpty) мы не сканируем его далеко вглубь, а просто проверяем на наличие хотя бы одной папки. Если диск имеет хотя бы одну папку, то вставляем внутрь соответствующего ей узла пустой элемент (Insertltem ("", 0, 0, h)), который дает возможность впоследствии раскрыть (expand) данный узел. Затем, когда пользователь действительно его раскроет, мы обработаем это событие и удалим пустой элемент. Вместо него наполним раскрытую ветвь реальными сущностями. Этот прием обеспечивает постепенное наполнение дерева по сценарию, определяемому действиями пользователя.

    Примечание
    Примечание

    Сначала я написал рекурсивную функцию анализа и заполнения всего файлового дерева при начальном запуске приложения. Оказалось, что эта процедура занимает 5-7 минут, в течение которых приложение выглядит мертвым. Правда, после нее дерево раскрывает свои ветви мгновенно, так как оно уже хранит информацию обо всех своих ветвях. В выбранном варианте работы с деревом вновь раскрываемые ветви вносят некоторую задержку, но после схлопывания (collapse) какой-либо ветви ее повторное раскрытие происходит быстро, так как информация уже имеется в дереве, точнее в элементе CTreeCtrl Другим вариантом решения проблемы является параллельное сканирование файлового дерева в другом потоке приложения.
    Операция отсечения лишних символов нам понадобилась для того, чтобы из длинного файлового пути выделить только имя папки, которое должно появится в дереве справа от bitmap-изображения объекта — узла дерева. Мы решили показывать в дереве, в левом окне приложения, только папки. Файлы этих папок будут изображены в виде картинок в другом, правом, окне. Картинкой я называю содержимое документа в виде его чертежа — многоугольника (для простоты). Показывать будем только те файлы, которые соответствуют документам нашего приложения. Если вы помните, они должны иметь расширение mgn, как это было определено на этапе работы с мастером AppWizard.

    При усечении строки неоходимо использовать знание структуры файлового пути и методы класса cstring. Сначала отсекаем символ ' \' справа от имени папки, затем все символы слева от него. Существует и другой способ, использующий функцию _splitpath, справку по которой я рекомендую получить самостоятельно. В настоящий момент развития приложения строка sName может содержать только одно из имен логических дисков и большая часть кода работает вхолостую, но чуть позже, когда мы будем иметь дело с длинными файловыми путями, он заработает полностью.

    Для того чтобы оживить дерево в его начальном состоянии, осталось добавить код функции NotEmpty, которая проверяет текущий узел (файловый адрес папки) на наличие в нем вложенных папок и возвращает true в случае успеха и false, в случае если папка пуста или в ней присутствуют только файлы.

    Примечание
    Примечание

    Здесь важно отметить, что даже в пустой или вновь созданной папке всегда присутствуют два объекта файловой системы. Это так называемые «точки», или каталоги с именами: "." и "..". Они, возможно, знакомы вам со времен использования команд DOS.

    В библиотеке 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). Вы помните, что в «непустые» узлы мы вставляли нулевые элементы. Рекомендуем с



    Обзор функции Initlnstance

    Обзор функции Initlnstance

    Внесем некоторые изменения и сокращения в файл реализации класса CTreeApp. Откройте файл Тгее.срр в окне редактора и просмотрите коды функции Initlnstance. Если у вас присутствует блок кодов

    if (lAfxOlelnit()) {

    AfxMessageBox(IDP_OLE_INIT_FAILED) ;

    return 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.mgn\nTree.Document\nTree.Document

    Примечание
    Примечание

    Вы можете получить справку по всем частям этой строки, если вызовете помощь (Help) по индексу GetDocString — методу класса CDocTemplate, позволяющему выделить нужную подстроку комплексной строки.

    Если мы поместим в String Table новую строку с идентификатором !DR_DrawTYPE, то при открытии окон документов по шаблону m_pTemplDraw, они будут использовать этот ресурс. При вставке новой строки надо быть внимательным, так как ее индекс должен быть в определенном диапазоне.

  • Сделайте два щелчка (не двойной, а два щелчка с паузой между ними) в колонке Caption, той строки текста, которая идентифицирована IDR_TreeTYPE. При втором щелчке на месте выделенной строки появится окно редактирования.
  • Выделите, скопируйте в буфер всю текстовую строку и щелкните справа от окна редактирования.
  • Если фокус выделения ушел, то поставьте его вновь на строку IDR_TreeTYPE. Это обеспечит правильное значение индекса для нового ресурса. Вызовите контекстное меню и выберите команду New > String.
  • Появится новая строка, возможно, внизу экрана. Используя ту же технику повторного щелчка, создайте окно редактирования в новой строке и вставьте из буфера старый текст. Замените в нем две первые подстроки на Draw.
  • Задайте для новой строки идентификаторIDR_DrawTYPE и нажмите Enter.
  • Щелкните мышью заголовок столбца Value. Таблица будет отсортирована по возрастанию индексов.
  • Убедитесь в том, что индекс новой строки (видимо, 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);
    }

    Окна с геометрией данных

    Окна с геометрией данных
    Характерный для MFC двухступенчатый способ создания окна cwndGeom объясняется тем, что с каждым окном связаны две сущности: Windows-окно, характеризуемое описателем окна, и объект класса cwndGeom, который мы еще должны разработать. В коде функции show для каждого полигона сначала динамически создается объект класса cwndGeom (конструктор класса), а затем — управляемое им Windows-окно (Create). При создании объекта мы передаем ему указатель на класс родительского окна и индекс полигона в контейнере. Поэтому окно впоследствии сможет найти нужный полигон в документе и изобразить его в своем контексте. Мы запоминаем адреса всех объектов CwndGeom в массиве m_pWnds, для того чтобы потом можно было уничтожить все Windows-окна (вызвав DestroyWindow), так же, как и все объекты класса cwndGeom (вызвав деструктор класса CWndGeom). Эту процедуру надо выполнять каждый раз, когда пользователь выбирает новый узел в файловом дереве.

    Вам уже знакома процедура ввода в проект новых классов. Сейчас настала пора применить ее для ввода в проект класса cwndGeom. При работе с мастером MFC Class Wizard выберите в качестве базового класс cwnd и измените предлагаемые по умолчанию имена файлов, в которых будут размещены стартовые коды нового класса. Вместо WndGeom.h и WndGeom.cpp задайте RightView.h и RightView.cpp. После того как мастер закончит работу, вставьте в начало файла. RightView.h упреждающее объявление class CWndGeom; так как класс CRightview содержит массив указателей этого типа, а его объявление стоит до объявления cwndGeom.

    Примечание
    Примечание

    Надо отметить, что в бета-версии Studio.Net описываемый способ размещения нового класса работает неверно и для исправления ситуации мне пришлось убрать вновь добавленную директиву #pragma once из файла RightView.h и три новые, вставленные мастером, директивы #include из файла RightView.cpp. Надеюсь, что у вас ошибок такого рода не будет.

    Изготовленная мастером заготовка класса содержит несколько больше элементов, чем нам необходимо. В частности, не нужны макросы DECLARE_DYNAMIC и IMPLEMENT^ DYNAMIC, так как мы не собираемся использовать унаследованную от CObject функцию isKindOf. Посмотрите справку по концепции наследования от CObject, чтобы понять, как связаны макросы с функцией IsKindOf, затем уберите макросы и внесите изменения в интерфейс класса так, чтобы он был:

    class 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".

    Отслеживание состояния команд

    Отслеживание состояния команд
    Текущее состояние команды меню или кнопки на панели инструментов легко определяется по их облику: недоступная команда или кнопка имеет блеклый (grayed) вид. Кроме того, бывают команды, работающие по принципу переключателя (включен — выключен). Их состояние определяется по наличию — отсутствию флажка слева от команды меню или утопленному — нормальному облику кнопки.

    Отслеживание состояния команд производится каркасом приложения в паузах между обработкой сообщений. В эти моменты вызывается виртуальная функция Onldle, и если вы ее переопределите для выполнения какой-либо своей фоновой задачи, то можете нарушить или замедлить процесс отслеживания состояния команд. Логика перехода между состояниями определяется специальными функциями т- обработчиками событий UPDATE_COMMAND_UI. Мы должны создать такой обработчик для отслеживания состояния команды ID_EDIT_NEWPOLY. Схема создания точно такая же, как и для самой команды, за исключением того, что вы вместо строки COMMAND выбираете строку UPDATE_COMMAND_UI:

    void 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 и наоборот.
    Примечание
    Примечание

    Здесь важно понять, что фокус на самом деле попадает в одно из дочерних окон CLeftView или CRightView или CDrawView. Но это происходит после того, как он попадет в родительское окно-рамку. В принципе, возможны и другие варианты решения проблемы своевременной смены панелей инструментов. Например, переопределить в каждом из трех представлений виртуальную функцию OnActivateView и в ней вызывать ChangeToolbar.

    Заметьте, что фокус может быть переведен в окно четырьмя разными способами:
  • активизация представления или его рамки при помощи левой кнопки мыши;
  • ввод клавишной комбинации (accelerator) Ctrl+F6, которая обрабатывается каркасом приложения и по очереди в цикле активизирует окна;
  • системная активизация следующего окна при закрытии одного из окон;
  • системная активизация окна при создании одного из окон (вспомните вызов CreateNewFrame В теле CTreeDoc: :MakeView) или открытии существующего документа.
  • Во всех четырех случаях окну-рамке будет послано сообщение 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. Нам понадобятся:
  • массив (контейнер) полигонов, которые соответствуют файлам документов, обнаруженных в текущем каталоге;
  • массив строк текста с файловыми путями этих документов;
  • один «дежурный» полигон, который в данный момент редактируется, то есть выбран для демонстрации в окне третьего представления (CDrawView);
  • размеры документа в логической системе координат (Page space);
  • коэффициент увеличения размеров при переходе из World в Page-пространство.
  • Кроме этого, нам понадобятся методы для управления тремя окнами: 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 m_sFiles;

    //====== Размер документа в 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

    Реакция на уведомляющие сообщения CTreeCtrl
    Когда пользователь раскрывает узел дерева, то встроенный в класс CTreeView объект класса CTreeCtrl посылает родительскому окну (нашему представлению CLef tview) уведомляющее сообщение. Оно работает по схеме WM_NOTIFY, которую мы уже рассматривали. Наш класс CLef tview должен реагировать на это сообщение, сканировать раскрываемую папку или логический диск и изменять дерево, вставляя в раскрываемую ветвь новые объекты файловой системы, которые обнаружены внутри папки или диска. Для того чтобы ввести в класс способность реагировать на рассматриваемое сообщение, вы должны:

  • Открыть окно Class View, установить фокус на имя класса CLeftView и перейти в окно Properties.
  • В окне Properties нажать кнопку с подсказкой Messages, а затем кнопку Categorized.
  • Нажать маркер (-) Common в верхнем левом углу прокручиваемого списка сообщений так, чтобы он изменился на (+). Тем самым вы скрываете часть списка с перечнем всех обычных сообщений Windows.
  • В оставшейся части списка найти сообщение =TVN_ITEMEXPANDING и в выпадающем списке справа выбрать действие .
  • Повторить действия пункта 4 для сообщений =TVN_ITEMEXPANDED и =TVN_ SELCHANGED.
  • Буква 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.

    Реакция на выбор узла дерева

    Реакция на выбор узла дерева

    Поиск «своих» файлов, то есть файлов с расширением mgn, и демонстрацию их содержимого в виде окон с рисунками следует производить в ответ на выбор (selection) пользователем одного из объектов файлового дерева. Это действие отличается от раскрытия узла дерева, когда пользователь однократно нажимает на маркер (+) раскрытия или делает двойной щелчок на самом узле. Для того чтобы выбрать тот или иной узел, пользователь либо щелкает мышью изображение, либо текстовую строку, соответствующую данному узлу. Реакцию на уведомление об этом событии (OnSelchanged) мы уже ввели в.состав класса CLeftview. Теперь введите внутрь этой функции следующие коды:

    void 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. При изменении выбора пользователя мы:

  • уничтожаем предыдущие данные с помощью метода FreeDocs;
  • ищем и запоминаем свои файлы в выбранном объекте файловой системы (метод SearchForDocs);
  • создаем новые объекты вспомогательного класса CPolygon;
  • просим их прочесть свои данные в обнаруженных файлах;
  • отображаем их в виде картинок в окне правого представления с помощью метода ProcessDocs.
  • Поиск документов своего типа (mgn-файлов) производится по той же схеме с использованием класса CFindFile и его методов поиска объектов файловой системы. Но если ранее мы просматривали все объекты, задав маску поиска "*.*", то теперь мы можем сузить поиск, задав маску "* .mgn". Полные пути найденных файлов будем хранить в контейнере m_sFiles типа vector, который чуть позже мы вставим в число членов класса документа. А сейчас дополните класс CLef tview методом:

    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());
    }
    }

    Редактируемый полигон

    Рисунок 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-координаты вершин полигона?

    Ресурсы шаблона документов

    Ресурсы шаблона документов
    Если мы не поленимся и создадим для второго шаблона документов все остальные перечисленные выше ресурсы, то приложение действительно будет вести себя в соответствии с концепцией MDI, так как она трактуется компанией Microsoft. Это означает, что приложение будет следить за типом активного документа и автоматически изменять связанные с ним ресурсы (значок и меню). К сожалению, автоматическая смена панели инструментов все-таки потребует некоторых усилий. Имя текущего документа совпадает с именем файла, в котором он хранится, но если документ еще не был сохранен, то его имя генерируется автоматически и также зависит от выбранного шаблона. Перейдите в окно Resource View и откройте в дереве ресурсов узел Icon. Вы видите индексы двух значков, которые сгенерированы мастером AppWi zard и служат для идентификации приложения (IDR_MA IN FRAME) и его документов (IDR_TreeTYPE). При желании вы можете отредактировать изображения, открыв их в окне редактора или, что проще, заменив их на другие. Техника замены проста: открыть готовое изображение в рамках Studio.Net (их много по адресу ..\Microsoft Visual Studio.Net \ Common7\ Graphics\icons), скопировать в буфер, открыть существующий значок, нажать Delete и Ctrl+V. He забывайте, что на самом деле имеется 4 значка (2 маленьких и 2 больших). Переход между изображениями значков разных размеров производится в диалоге, вызываемом командой Image > Open > Image > Туре. Команда доступна, когда курсор стоит в окне редактора изображений. Теперь опишем, как добавить еще один значок.

  • Вызовите контекстное меню на узле дерева IDR_TreeTYPE и дайте команду Insert Copy.
  • В окне диалога измените язык ресурса на любой другой, нажмите ОК, переведите курсор на новый узел дерева ресурсов и перейдите в окно Properties.
  • Измените идентификатор на IDR_DrawTYPE и верните язык. К сожалению, в моей версии изменения происходят только после того, как будет дана команда Save.
  • Замените новые изображения, большое и маленькое, на какие-то другие.
  • Откройте в окне редактора меню IDR_TreeTYPE. Удалите из меню File команды Print, Print Preview, Print Setup и один разделитель (Separator). Повторите действия по копированию ресурсов и сделайте копию всего меню IDR_TreeTYPE. Поменяйте идентификатор копии на IDR_DrawTYPE. Откройте копию меню в окне редактора и уберите из него команду View > Split и добавьте View > Documents с идентификатором ID_VIEW_TOGGLE. В пункт меню Edit добавьте две новые команды: New Poly и Poly Color. ... Вновь откройте меню IDR_TreeTYPE, удалите в нем весь пункт меню Edit, добавьте команды View > Geometry с идентификатором ID_VIEW_TOGGLE и View > Refresh с идентификатором ID_VIEW_REFRESH. Команда ID_VIEW_70GGLE будет служить для переключения между двумя окнами CTreeFrame и CDrawFrame, содержащими три представления одного и того же документа.
    В том и другом из рассматриваемых меню измените команду Window > Tile на Window > Tile Horizontally и добавьте новую команду Window > Tile Vertically, выбрав из выпадающего списка идентификатор ID_WINDOW_TILE_VERT. Обработчики этих команд уже существуют в каркасе приложения, поэтому нам не нужно их создавать.
    Откройте инструментальную панель (Toolbar) IDR_MAINFRAME. Удалите из нее кнопки вырезания, копирования, вставки и печати. Используя технику копирования, добавьте две новые инструментальные панели: IDR_TreeTYPE и IDR_DrawTYPE. В последнюю вставьте две новые кнопки, соответствующие двум командам меню: New Poly и Poly Color.. .. Добавьте два новых курсора, которые будут использованы в разные моменты жизни приложения. Один будет загружаться автоматически при попадании фокуса в окна демонстрации содержимого документов (типа CWndGeom). Другой будет использован в режиме визуального редактирования данных документа.
  • Вызовите контекстное меню в окне Resource View и дайте команду Add Resource.
  • В списке диалога выберите Cursor и нажмите кнопку New.
  • Задайте идентификатор IDC_MYHAND и скопируйте изображение курсора H_POINT.CDR из папки Cursors студии.
  • Повторите эти действия для создания курсора IDC_MOVE с изображением, скопированным из файла 4WAY02.CUR.
  • Если вы вместо IDC_MYHAND зададите IDC_HAND, то компилятор многократно сообщит о переопределении стандартного курсора с таким индексом. Внесите еще одно изменение, связанное с ресурсами. Так как мы сами будем перезагружать инструментальные панели, то надо упростить доступ к ним. В файле MainFrm.h перенесите следующие два объявления из секции protected в секцию public:
    public:
    CToolBar m_wndToolBar;
    CStatusBar m_wndStatusBar;


    Создание и связывание переменных с полями диалога

    Создание и связывание переменных с полями диалога

    Для обмена данными с окнами редактирования следует в классе диалога CPolyDlg создать переменные. Это удобно делать с помощью мастера Studio.Net Add Member Variable.

  • В окне редактора откройте форму диалога IDD_POLYCOLOR, поставьте фокус в поле IDC_RED и, вызвав контекстное меню, дайте команду Variable.
  • В окне мастера включите флажок Control Variable, переключатель Control-Value поставьте в положение Value, а в других окнах задайте следующие опции:
  • Control ID: IDC_PEN.
  • Access: public.
  • Variable Type: UINT.
  • Variable Name: m_nPen.
  • Нажмите кнопку Finish.
  • В результате работы мастера в классе CPolyDlg должна появиться новая переменная m_nPen типа UINT, которая будет хранить толщину пера текущего полигона и обмениваться числовым значением с полем IDC_PEN диалога. Обмен происходит при вызове функции:

    DDX_Text(pDX, IDC_PEN, m_nPen);

    который происходит в теле функции DoDataExchange. Указанная строка программы была автоматически вставлена мастером Member Variable wizard.

    Примечание
    Примечание

    Здесь я вынужден просить прощения у читателя за неполную информацию, так как моя бета-версия Studio.Net не позволяет автоматизировать процесс создания переменных и связывания их с элементами управления диалога в той степени, в которой это должно быть. Я уверен, что в той версии, с которой будете иметь дело вы, этот процесс будет более эффективным. А сейчас вынужден закрывать и вновь открывать мастер для каждой новой переменной. Мне ничего не остается, кроме как посоветовать повторить вышеописанный процесс для создания еще трех переменных (m_nRed, m_nGreen, m_nBlue) того же типа UINT, но связанных с другими окнами.
    Для синхронизации положений ползунков со значениями в окнах редактирования необходимо создать еще четыре переменные (m_cColor, m_cRed, m_cGreen и m_cBiue), связанные с теми же окнами IDC_COLOR, IDC_RED, IDC_GREEN и IDC_BLUE. На сей раз переключатель Control-Value должен быть установлен в положение Control, а в поле Variable Type должен быть указан или выбран наш новый класс CClrEdit. Используя ту же технику, создайте три переменные (m_rSlider, m_gSlider и m_bSlider) типа Control и свяжите их с тремя ползунками. При этом в поле Variable Type: должен быть выбран класс CSliderCtrl.

    Ограничения на числовые значения, вводимые пользователем в окна редактирования, реализуются с помощью функций динамической проверки данных. Это функции типа DDV_ (Dynamic Data Validation), которые, так же как и функции DDX_, создаются с помощью мастера Member Variable Wizard. Однако эту часть работы в бета-версии Studio.Net приходится делать вручную. Вам придется самостоятельно найти справку по использованию мастера для автоматизации создания функций проверки данных. Тема в указателе справочной системы обозначена как Dialog Data Exchange and Validation. Важной особенностью использования функций типа DDV_ является то, что для каждого элемента управления вызовы DDV_ -функций должны непосредственно следовать за вызовами оох_-функций. Нам надо задать ограничения на значения цвета (0-255) и значение толщины пера (0-100). В конечном счете функция DoDataExchange должна приобрести вид:

    void 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. Для этого:

  • Установите фокус на узел Тгее.гс в окне Resource View и вызовите контекстное меню.
  • Выберите команду Add Resource, в окне появившегося диалога выберите элемент Bitmap и нажмите кнопку New.
  • Перейдите в окно Properties. Для удобства вытащите его из блока окон (команда Floating) и отбуксируйте в сторону. В окне задайте идентификатор ID = IDB_1.
  • Установите фокус на пустом поле будущего изображения. При этом содержимое окна Properties изменится, позволив вам задать размеры (Height = 16, Width = 16).
  • Средствами редактора создайте изображение. Для создания второго изображения можно воспользоваться копией первого. В окне Resource View установите фокус на узел дерева ресурсов, соответствующий первому изображению, и вызовите контекстное меню.
  • В этом меню выберите команду Insert Copy, затем в появившемся окне диалога измените язык на любой, отличный от текущего (он выведен в окне Language).
  • Выполните двойной щелчок на новом узле дерева ресурсов в окне Resource View, измените изображение, вновь переведите фокус на узел дерева и перейдите в окно Properties.
  • Измените IDB_1 на IDB_2 и при желании возвратите язык, заменив на тот, который принят по умолчанию.
  • Повторив эту процедуру столько раз, сколько необходимо иметь различающихся изображений, закончите тем, что последнему из них присвойте ID = IDB_N. Имена идентификаторов произвольны. Важно только то, что их числовые эквиваленты должны следовать подряд. Если вы не отрывались на создание других ресурсов, то Studio.Net сделала это автоматически. Будем считать, создано 3 изображения, и индекс последнего из них равен IDB_3. Для того что бы связать список с деревом вместо строки m_Tree. Insert I tern ("Item", 0,0); В функцию OnlnitialUpdate вставьте такой фрагмент:
    //====== Традиционный для MS двухступенчатый способ

    //====== создания нового объекта - списка изображений

    m_pImgList = new CimageList;
    m_pImgList->Create(16, 16, ILC_MASK, 0, 32);

    for (UINT nID = IDB_1; nID <= IDB_3; nID++)
    {

    //====== Временный объект

    CBitmap bitmap;

    //====== Загрузка из ресурсов

    bitmap.LoadBitmap(nID);

    //====== Добавление в конец списка изображений

    m_pImgList->Add(Sbitmap, (COLORREF)OxFFFFFF);

    //====== Освобождаем память
    bitmap.DeleteObject();
    }
    //=== Связывание списка изображений с объектом
    CTreeCtrl m_Tree.SetlmageList(m_pImgList, TVSIL_NORMAL);

    Параметры функции Create задают размеры изображений, их тип, начальный размер списка и квант его приращения при вставке новых изображений. Цикл загрузки изображений и вставки их в список будет корректно работать, только если их индексы следуют подряд. Метод SetlmageList связывает список с деревом, то есть элементом управления m_Tree типа CTreeCtrl. После этого можно начать формировать дерево.
    Вставку новых ветвей осуществляют несколькими способами. Рассмотрим один из них, использующий специальную структуру типа TVINSERTSTRUCT. Просмотрите справку по этому типу, чтобы знать состав полей структуры. Обычно необходима одна глобальная структура такого типа. Это удобно, так как ею могут пользоваться несколько разных функций. В начало файла LeftView.cpp (после директив препроцессора) вставьте определение:
    TVINSERTSTRUCT gtv; // Глобальная структура
    Вернемся К функции 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 и вновь посмотреть, что получится. Обратите внимание на то, что изображения изменяются при выборе узлов, то есть при переводе курсора мыши с одного узла на другой.



    Тестирование

    Тестирование

    Приложения, даже если они на первый взгляд функционируют корректно, надо тщательно тестировать, с тем чтобы проверить максимальное число состояний и ситуаций, в которых оно может оказаться. Так, тестируя настоящее приложение, я обнаружил два дефекта (не удивлюсь, если вы найдете еще больше). Первый состоит в том, что, если до выполнения команды Refresh изменить вручную позицию полос прокрутки, после выполнения команды происходит рассинхронизация полос. Лекарство оказалось простым — вставить в нужное место строку с вызовом:
    ScrollToPosition(CPoint(0,0));
    Эта функция является методом класса CScrollview, она устанавливает обе полосы прокрутки в исходные состояния. Определение места вставки мы оставляем читателю.
    Кроме того, логика приложения нарушается, если окна мини-чертежей (cwndGeom) видны не полностью, а только частично. Курсор в этом случае не изменяет свою форму, несмотря на то что он выходит за границы окна CRightView, то есть за границы клиентской области окна CTreeFrame. Этот эффект наблюдается только при выходе в сторону гипотетического продолжения окна cwndGeom. Объяснение в том, что мы захватили мышиные сообщения (SetCapture) и направляем их в частично скрытое окно типа CWndGeom, которое не отпускает мышь, так как справедливо считает, что курсор находится над его прямоугольником. Окно не знает, что та его часть, которая находится под курсором, в данный момент скрыта окном-рамкой или полосами прокрутки. Вы помните, что полосы прокрутки являются частью клиентской области окна? Если диагноз поставлен точно, то и лечение будет эффективным. Ниже приведена новая версия обработки сообщения

    WM_MOUSEMOVE В Классе 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. Три представления одного документа

    Три представления одного документа

    Управление файловым деревом

    Управление файловым деревом
  • Настройка стартового кода
  • Список изображений, ассоциируемый с деревом
  • Обращаемся к операционной системе
  • Реакция на уведомляющие сообщения CtreeCtrl
  • Класс CPolygon
  • Развитие класса документа
  • Взаимодействие представлений документа
  • Немодальный диалог
  • В этом уроке мы подробно рассмотрим процесс разработки MDI-приложения, в котором один тип документов взаимодействует с несколькими своими представлениями. В рамках архитектуры «документ — представление» принято использовать следующие термины:
  • документ — обозначает класс, производный от MFC-класса CDocument и вобравший в себя (инкапсулирующий) функциональность данных документа;
  • представление — обозначает класс, производный от MFC-класса cview и инкапсулирующий функциональность окна, дочернего по отношению к окну-рамке. В нем в том или ином виде представлены данные документа.
  • Главным моментом в архитектуре является то, что один документ может иметь несколько связанных с ним представлений, но каждое из них может быть связано лишь с одним документом.
    Особенностью разрабатываемого приложения является то, что в одном из представлений, управляемых классом cscrollview, пользователь сможет просматривать в качестве «картинок» — чертежей или схем, выбирать и открывать документы своего приложения, которые расположены в файлах с различными адресами. Навигацию по файловому дереву будем осуществлять с помощью второго представления, которым управляет класс CTreeView. Классы CScrollView и CTreeView являются специализированными потомками класса cview. Класс CTreeView тесно связан с классом CTreeCtrl, который разработан как элемент управления произвольным деревом. Мы должны научиться им управлять.
    Документ, выбранный пользователем с помощью двух предыдущих представлений, отображается в третьем, производном от cview, которое служит посредником между пользователем и данными документа. В его окне пользователь сможет редактировать данные документа. В качестве данных мы используем динамический массив (контейнер) точек с вещественными координатами, который удачно моделирует произвольный чертеж — двухмерную проекцию какого-либо элемента конструкции. Идеи, заложенные в этом учебном приложении, использованы в реальном проекте по расчету физических полей, описываемых дифференциальными уравнениями в частных производных. В частности, производились расчеты поля магнитов, отсюда проистекает выбранное нами расширение (mgn) для документов приложения. В задачах такого рода исходными являются данные о геометрии расчетной области. Именно она наиболее точно определяет документ (вариант расчета). Если число таких геометрий велико, то поиск варианта по картинке расчетной области существенно упрощает жизнь исследователя физических полей. В связи с этим был получен заказ — ввести в проект возможность поиска и выбора документа по миниатюрному графическому представлению (схеме) геометрии расчетной области. Упрощенная реализация этой части проекта рассмотрена ниже. Начнем с создания стартовой заготовки MDI-приложения.
  • На странице VS Home Page выберите команду (гиперссылку) Create New Project.
  • В окне диалога New Project выберите уже знакомый вам тип проекта: MFC Application, задайте имя проекта Tree и нажмите ОК.
  • В окне мастера MFC Application Wizard выберите вкладку Application Type и сделайте следующие установки: Multiple documents. Windows Explorer, Document/View procedure support, use MFC in a shared DLL
  • Перейдлте на другую страницу мастера (Document Template Strings) и в поле File extension: задайте расширение mgn для файлов документов будущего приложения.
  • На странице User Interface Features поставьте флажок Child maximized, для того чтобы окна документов занимали всю клиентскую область главного окна приложения. Там же установите флажок Maximized, для того чтобы само главное окно занимало весь экран.
  • Так как мы собираемся вводить в проект новые классы для управления окнами, различным образом представляющими документ, целесообразно изменить предлагаемые мастером имена классов. На странице Generated Classes измените имена: CTreeView на CRightView, CChildFrame на CTreeFrame. Будет удобнее ориентироваться в файлах проекта, если изменить также и имена файлов ChildFrm на TreeFrm, TreeView на RightView (для h- и срр-файлов).
  • Для класса CRightView произведите замену родителя (в поле Base Class). Вместо CListView выберите класс CScrollView.
  • Нажмите кнопку Finish.


  • Управление с помощью немодального диалога

    Рисунок 5.6. Управление с помощью немодального диалога

    Управление с помощью немодального диалога Изменения такого рода, как вы уже догадались, влекут за собой достаточно много ошибок на стадии компиляции, если не уделить внимания проблеме видимости классов. Так, надо вставить упреждающее объявление (class CPolyDlg;) в файл с интерфейсом документа и директиву #include "PolyDlg.h" в файл с его реализацией. Кроме того, при работе с диалогом в немодалыюм режиме надо помнить о том, что для его окна свойство Visible должно быть установлено в True. По умолчанию это свойство выключено, так как при запуске диалога в модальном режиме диалог сначала невидим, но .затем функция DoModal вызывает showWindow с параметром SW_SHOW, что активизирует окно, делая его видимым. Мы тоже можем поступить так же, вставив аналогичный вызов после вызова функции Create, но проще сразу установить для диалога (в категории Behavior окна Properties) свойство Visible.

    В настоящий момент приложение может быть запущено и при условии отсутствия ошибок протестировано. Команда запуска диалога должна быть доступна, только когда активно окно CDrawFrame, или, точнее, фокус ввода принадлежит представлению, управляемому классом CDrawView. Проверьте все варианты запуска диалога: с помощью команды меню или кнопки на панели инструментов. Проверьте также возможность перевода фокуса в любое из представлений документа при наличии окна диалога (Рисунок 5.6).

    Вид главного окна приложения Tree

    Рисунок 5.1. Вид главного окна приложения Tree

    Вид главного окна приложения Tree

    Вид окна диалога

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

  • Выберите в контекстном меню команду Add Class.
  • В левом окне диалога Add Class раскройте дерево Visual C++, сделайте выбор MFC * MFC Class и нажмите кнопку Open.
  • В окне мастера MFC Class Wizard задайте имя класса CPolyDlg, в качестве базового класса выберите CDialog. При этом станет доступным поле Dialog ID.
  • В это поле введите или выберите из выпадающего списка идентификатор шаблона диалога IDD_POLYCOLOR и нажмите кнопку Finish.
  • Просмотрите объявление класса 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.

    Примечание
    Примечание

    Обратите внимание на символ = перед идентификатором сообщения Windows. Он необходим, чтобы различить два сообщения с одним именем. Наличие символа = означает принадлежность сообщения к группе отражаемых (reflected) сообщений.

    Применяя уже известный вам подход, создайте класс cclrEdit с базовым классом CEdit. В процессе определения атрибутов нового класса укажите существующие файлы (PolyDlg.h и PolyDlg.cpp) в качестве места для размещения кодов нового класса. Если возникнут окна диалогов с просьбой подтвердить необходимость погружения кодов в уже существующие файлы, то ответьте утвердительно. Введите изменения в файл PolyDlg.h, так чтобы он приобрел следующий вид:

    #pragma once

    //===== Класс нестандартного окна редактирования
    class CClrEdit : public CEdit
    {
    DECLARE_DYNAMIC (CClrEdit)

    public:

    CClrEdit () ;

    virtual -CClrEdit () ;

    void ChangeColor (COLORREF clr) ; // Изменяем цвета

    protected:

    DECLARE_MESSAGE_MAP ()

    private :

    COLORREF ra_clrText; // Цвет текста

    COLORREF ra_clrBk; // Цвет фона

    CBrush m_brBk; // Кисть для закраски фона
    };

    //====== Класс для управления немодальным диалогом

    class CPolyDlg : public CDialog
    {

    friend class CClrEdit;

    DECLARE_DYNAMIC (CPolyDlg)

    public : enum ( IDD = IDD_POLYCOLOR } ;

    //====== Удобный для нас конструктор

    CPolyDlg (CTreeDoc* p) ;
    virtual -CPolyDlg ( ) ;

    //====== Отслеживание цвета

    void UpdateColor () ;

    protected: virtual void DoDataExchange (CDataExchange* pDX) ;

    DECLARE_MESSAGE_MAP ( ) private :

    CTreeDoc* m_pDoc; // Обратный указатель

    CBitmapButton m_cTri; // Кнопки с изображениями

    CBitmapButton m_cPent;

    CBitmapButton m_cStar;

    bool ra_bScroll; // Флаг использования ползунка };
    };

    Мы изменили конструктор класса CPolyDlg так, чтобы он имел один параметр — адрес документа, который мы используем в качестве обратного указателя. Это поможет нам управлять приложением, оставаясь в рамках методов диалогового класса. Теперь воспользуемся услугами Studio.Net для создания функции-обработчика сообщения =WM_CTLCOLOR в классе нестандартного окна редактирования.

  • Поставьте фокус на элемент CClrEdit дерева классов в окне Class View, перейдите в окно Properties и нажмите кнопку Messages.
  • Нажмите кнопку Categorized и, нажав на маркер (-) Common, закройте список обычных сообщений.
  • В оставшейся части списка Reflected найдите сообщение =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.

    Вид расщепленного окна с файловым деревом

    Рисунок 5.2. Вид расщепленного окна с файловым деревом

    Вид расщепленного окна с файловым деревом учебными целями ввести исправления и добиться демонстрации не только папок, но и файлов. Убедитесь в том, что различным типам файлов соответствуют разные изображения. Они, как вы помните из третьего урока и знаете из опыта общения с Windows, определены на этапе регистрации значка приложения или его документа.

    Визуальное редактирование данных

    Визуальное редактирование данных

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

    Windows Explorer мы даем возможность пользователю выбрать документ не по его имени и значку, а по его содержимому в виде чертежа конструкции.

    Современным подходом к редактированию данных является использование таблиц (grids) типа Excel, в которых отражены данные открытого документа и которые позволяют редактировать их, мгновенно получая обратную связь в виде изменившейся геометрии устройства. Таблицы удобно разместить на одной из панелей расщепленного окна с регулируемой перегородкой (split bar).

    К сожалению, в MFC нет классов, поддерживающих функционирование таблиц. Реализация их в виде внедряемых СОМ-объектов обладает рядом недостатков. Во-первых, существующие grid-элементы обладают весьма ограниченными возможностями. Во-вторых, интерфейсы обмена данными между внедренной (embedded) таблицей и приложением-контейнером громоздки и неуклюжи. Самым лучшим, известным автору, решением этой проблемы является использование библиотеки классов objective Grids, разработанных компанией stingray Software. Библиотека полностью совместима с MFC. В ней есть множество классов, поддерживающих работу разнообразных элементов управления: combo box, check box, radio button, spinner, progress и др. Управление grid-элементами или окнами типа CGXGridWnd на уровне исходных кодов дает полную свободу в воплощении замыслов разработчика.

    Однако, не имея лицензии на использование данного продукта, я не могу использовать его в разработке даже этого учебного приложения. Поэтому мы пойдем традиционным путем и внесем в проект возможность визуального редактирования данных с помощью обычных мышиных манипуляций. Представление, поддерживаемое классом CDrawView, как было уже отмечено, должено служить посредником между пользователем и данными текущего полигона.

    Изменение координат вершин полигона в диапазоне, ограниченном размерами логической области (2000x2000), можно производить простым перетаскиванием его вершин с помощью указателя мыши. Чтобы намекнуть пользователю нашего приложения о возможности произведения таких операций (вряд ли он будет читать инструкцию), мы используем стандартный прием, заключающийся в изменении формы курсора в те моменты, когда указатель мыши находится вблизи характерных точек изображения. Это те точки, которые можно перетаскивать. В нашем случае — вершины полигона. Очевидной реакцией на курсор в виде четырех перекрещенных стрелок является нажатие левой кнопки и начало перетаскивания. Заканчивают перетаскивание либо отпусканием кнопки мыши, либо повторным ее нажатием. Во втором варианте при перетаскивании не обязательно держать кнопку нажатой. Остановимся именно на нем.

    В процессе перемещения можно постоянно перерисовывать весь объект, что обычно сопровождается неприятным мельканием, а можно пользоваться приемом, сходным с технологией rubber-band (резиновая лента). Вы используете ее, когда выделяете несколько объектов на рабочем столе Windows. Прием характеризуется упрощенной перерисовкой контура перемещаемого объекта. При этом объект обыч-

    но обесцвечивается. Такую функциональность мы уже ввели в класс CPolygon. Тонким местом в этой технологии является особый режим рисования линий контура. Каждое положение перемещаемой линии рисуется дважды. Первый раз линия рисуется, второй — стирается. Этот эффект достигается благодаря предварительной настройке контекста устройства, которую производит функция SetROP2. Если вызвать ее с параметром R2_xoRPEN, то рисование будет происходить по законам логической операции XOR (исключающее ИЛИ). В булевой алгебре эта операция имеет еще одно имя — сложение по модулю два. Законы эти просты: 0+0=0; 0+1 = 1; 1+0=1; 1 + 1=0. Ситуацию повторного рисования можно представить так:

  • цвет каждого пиксела (каждой точки растра) при рисовании определяется путем суммирования цвета фона и цвета пера по законам операции XOR;
  • если перо красное (8 младших бит цвета установлены в 1), а фон белый (то есть присутствуют все 3 компонента цвета — 3 байта установлены в 1), то результатом операции XOR будет цвет Cyan, так как красный компонент исчезнет (1+1=0). Оставшиеся же компоненты, зеленый и синий, дают цвет Cyan;
  • если еще раз пройтись красной линией по тому же месту (по линии цвета Cyan), то при сложении цветов единицы попадут на нули и цвет будет белый (все 3 байта станут равны 1).
  • Итак, повторный проход стирает линию. В качестве упражнения повторите выкладки при условии, что перо белое (затем — черное). Такие упражнения шлифуют самое главное качество программиста — упорство. При черном пере вы должны получить что-то не то. Тем не менее мы берем черное перо, но при этом задаем стиль PS_DOT, что в принципе равносильно черно-белому перу. Белые участки работают как описано, а черные своей инертностью помогают создать довольно интересный эффект переливания пунктира или эффект натягивания и сжимания резинки. Есть еще одно значение (К2_ыот) параметра функции SetROP2, которое работает успешно, но не без эффекта резинки.

    Примечание
    Примечание

    Я думаю, что цифра 2 в имени функции означает намек на фонетическую близость английских слов «two» и «to». Если предположение верно, то имя функции SetROP2 можно прочесть как «Set Raster Operation To», что имеет смысл установки режима растровой операции в положение (значение), заданное параметром функции. Обязательно просмотрите справку по этой функции (методу класса CDC), для того чтобы узнать ваши возможности при выборе конкретного режима рисования.
    Режим перетаскивания вершин полигона готов к использованию в момент вхождения указателя мыши в область чувствительности вершины (за этим следит флаг m_bReady). Кроме данного режима мы реализуем еще один режим — режим создания нового полигона (флаг m_bNewPoints), который вступает в действие при выборе команды меню Edit > New Poly. При анализе кода обратите внимание на то, что мы получаем от системы координаты точек в аппаратной системе, а запоминать в контейнере точек должны мировые (World) координаты. Преобразование координат осуществляется в два этапа:

  • сначала из Device-пространства в пространство Page (функция DPtoLP — Device Point to Logical Point);
  • затем из Page-пространства в пространство World (наша функция MapToWorldPt).
  • Теперь вы, вероятно, подготовлены к восприятию того, что происходит в следующих трех методах класса CDrawView. Первые два вы должны создать как реакции на сообщения WM_LBUTTONDOWN и WM_MOUSEMOVE, а последний (member function) — просто поместить в файл реализации класса, так как его прототип уже существует:

    void 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. Для этого:

  • Поставьте фокус на элемент CDrawView в представлении классов (Class View) и перейдите в окно Properties.
  • Нажав кнопку Events, выберите идентификатор ID_EDIT_NEWPOLY, раскройте маркер (+) и выберите COMMAND (первую из двух выпавших строк).
  • Создайте обработчик, выбрав в выпадающем списке справа от COMMAND.


  • Взаимодействие представлений документа

    Взаимодействие представлений документа
    В данный момент мы имеем три класса (CLef tview, CRightView, CDrawView) для управления тремя представлениями одного документа. Взаимодействие между ними должно быть реализовано с помощью методов класса CTreeDoc, так как именно документ поддерживает список всех своих представлений. Начнем с того, что обеспечим видимость классов, вставив в список директив препроцессора файла ТгееDос.срр еще две:

    #include "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, который выполняет указанное действие.
    Примечание
    Примечание

    Каркас MDI-приложения в принципе позволяет создать произвольное количество окон-двойников одного и того же документа и даже имеет для этой цели специальную команду (Window > New Window). Иногда это полезно, но в наш сценарий такая команда не вписывается. Поэтому мы ее убрали и пользуемся флагами m_bDrawExist и m_bTreeExist которые должны следить за ситуацией, чтобы не допустить дублирования окон.
    Вы помните, что в любой точке программы мы имеем право вызвать глобальную функцию 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 (AfxGetApp ());

    Всю работу по созданию окна-рамки и помещения в его клиентскую область выполняют методы 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 (GetActiveDocument());

    pDoc->m_bTreeExist = false;

    CMDIChildWnd::OnClose();
    }

    void CDrawFrame::OnClose()

    void CDrawFrame::OnClose()
    {

    CTreeDoc *pDoc = dynamic_cast (GetActiveDocument());

    pDoc->m_bDrawExist = false;

    CMDIChildWnd::OnClose() ;
    }

    Вы уже, наверное, привыкли к тому, что при введении функций-обработчиков, которые обращаются к объектам других классов приложения, надо корректировать директивы подключения заголовочных файлов. Вот и сейчас надо вставить директиву #include "TreeDoc.h" в файл реализации класса CDrawFrame.

    В настоящий момент приложение готово к запуску. Уберите временные комментарии, которые вставляли раньше, запустите приложение, устраните ошибки и протестируйте. Его поведение должно быть ближе к задуманному. Для проверки необходимо с помощью команды File > Save as записать некоторое количество документов, давая им различные имена. После этого следует убедиться, что каждый раз, как фокус выбора попадает в папку, где записаны документы, в правом окне появляются мини-окна типа cwndGeom с изображением полигона. При выборе одного их них щелчком левой кнопки мыши должно создаваться и активизироваться новое окно типа CDrawView. В этот момент полезно дать команду Window > Tile Horizontally, для того чтобы увидеть оба типа окон-рамок со всеми тремя представлениями одного документа. Если документы сохранить на гибком диске (и держать диск в дисководе), то они должны отображаются сразу после запуска приложения, так как сообщение =TVN_SELCHANGED поступает при инициализации левого окна.

    Cамоучитель по Visual Studio.Net

    Анимация

    Анимация

    На примере многочисленных хранителей экрана (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-буфере.
  • Создай в нем новое изображение.
  • Переключи буферы (front-back).
  • Последний шаг алгоритма не начнет выполняться, пока не закончится предыдущий шаг — создание нового кадра в 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.

    Другие функции OpenGL

    Другие функции OpenGL
    Другие функции OpenGL позволяют размещать объекты на трехмерной сцене, выбирать точку размещения глаза наблюдателя (камеру), передвигать эту точку. Неотъемлемой частью трехмерной графики является освещение материалов. Конвейер OpenGL использует специальные алгоритмы подсчета цвета любого фрагмента с учетом заданных свойств материала и источников света. Моделирование атмосферных эффектов (тумана, дыма, дымки) делает изображения более реалистичными. Функции моделирования тумана, дыма, загрязнений или просто эффекта присутствия воздуха можно найти в справочной системе по ключевому слову Fog.
    Механизм anti-aliasing сглаживает неровные края линий, отображаемых на компьютерном дисплее при низком графическом разрешении. Anti-aliasing является стандартной техникой в компьютерной графике и заключается в изменении цвета точек вблизи границ изломов. Техника Gouraud-тени сглаживает тень трехмерного объекта для достижения тонких различий цветов на специфической поверхности.
    Четвертая составляющая цветового кода RGBA носит название alpha. Alpha-смешивание позволяет комбинировать цвет обрабатываемого фрагмента с цветом точек, которые уже хранятся в буфере, моделируя тем самым прозрачность воздуха, стекла или другого материала. Этот эффект используют при демонстрации распределения поля внутри замкнутого объема. Достаточно дать пользователю возможность управлять степенью прозрачности границ, и он сможет рассматривать результаты вычислений (например, поверхности равного уровня искомого поля) внутри трехмерных объектов.

    OpenGL не предоставляет разработчику команд для описания сложных моделей. В ней есть примитивные геометрические объекты: точки, линии и многоугольники. Разработчик должен сам сконструировать модели, основываясь на этих нескольких простых примитивах. Но есть библиотеки, например классы Open Inventor, которые реализуют более сложные модели. Разработчик может использовать эти библиотеки для построения своих собственных.

    Конвейер OpenGL реализует процедурный, а не описательный подход к созданию изображения. Он выполняет функцию сервера, который обслуживает клиента — приложение, генерирующее последовательность команд. Коды сервера могут выполняться на другом компьютере, сервер может одновременно поддерживать несколько контекстов передачи OpenGL, а клиент может подключаться к любому из них. Однако система Windows полностью контролирует память, отводимую под буфер кадра (frame buffer). OpenGL может вступить в игру только после того, как Windows создаст и подготовит окно для передачи (rendering) изображения OpenGL.

    Двойная буферизация

    Двойная буферизация

    В настоящий момент перерисовка изображения во время манипуляций мышью очень плохая, так как мы работаем с одним (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; // Сдвигаем изображение вправо
    }

    При тестировании результата обратите внимание на поведение изображения. Например, чем больше сдвиг вправо, тем лучше видна левая боковая грань. Кажется, что совместно с перемещением объекта он поворачивается. Но это не так. Эффект объясняется особенностями перспективной проекции.

    Формирование массива вершин и индексов

    Формирование массива вершин и индексов

    Самой сложной задачей является правильное вычисление координат всех вершин треугольников и формирование массива индексов Tria, с помощью которого команда glDrawElements обходит массив Vert при задании треугольников. Функция Sphere реализует алгоритм последовательного обхода сначала всех сферических треугольников вокруг полюсов сферы, а затем обхода сферических четырехугольников, образованных пересечением параллелей и меридианов. В процессе обхода формируется массив вершин vert. После этого обходы повторяются для того, чтобы заполнить массив индексов Tria. Северный и южный полюса обрабатываются индивидуально. Для осуществления обхода предварительно создаются константы:

  • da — шаг изменения сферического угла а (широта),
  • db — шаг изменения сферического угла b (долгота),
  • af и bf — конечные значения углов.
  • Для упрощения восприятия алгоритма следует учитывать следующие особенности, связанные с порядком обхода вершин:

  • После обработки северного и южного полюсов мы движемся вдоль первой широты (a=da) от востока к западу по невидимой части полусферы и возвращаемся назад по видимой ее части. Затем происходит переход на следующую широту (а += da) и цикл повторяется.
  • Координаты вершин (х, z) представляют собой проекции точек на экваториальную плоскость, а координата у постоянна для каждой широты.
  • При обработке одной секции кольца для двух треугольников формируется по три индекса:
  • 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 + 1].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) ;
    }

    Запустите проект на выполнение и уберите возможные неполадки. Исследуйте функционирование программы, вводя различные значения глобальных параметров (регулировок). Попробуйте задать нечетное число секций. Объясните результат. В качестве упражнения введите возможность интерактивного управления степенью дискретизации сферы и исследуйте эффективность работы конвейера при ее увеличении.

    Формула учета освещенности

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

  • Mа — коэффициент отражения окружающего (ambient) света,
  • Md — коэффициент отражения рассеянного (diffuse) отражения,
  • Ms — коэффициент отражения зеркального (specular) отражения,
  • N— вектор нормали вершины, который задан командой glNormal,
  • V1— нормированный вектор, направленный от вершины к источнику света,
  • Ve — нормированный вектор, направленный от вершины к глазу наблюдателя,
  • h — блесткость (shininess) материала.
  • Члены в круглых скобках — это скалярные произведения векторов. Если они дают отрицательные значения, то конвейер заменяет их нулем. Alpha-компонент результирующего цвета освещения устанавливается равным alpha-компоненту диффузного отражения материала. Так как мы задали лишь один источник света (LIGHTO), то знак суммы можно опустить. Обратите внимание на то, что блесткость материала уменьшает (обостряет) пятно отраженного света, так как возведение в степень h > 1 чисел (v, -v,), меньших единицы, уменьшает их значение. Параллельные векторы v, и v, дадут максимальный вклад. Чем больше их рассогласование, тем меньший вклад даст последний член формулы.



    Графика OpenGL

    Графика OpenGL
  • Обзор возможностей библиотеки OpenGL
  • Перспективная проекция
  • Вносим свет
  • Интерактивное управление положением и ориентацией
  • Двойная буферизация
  • Строим икосаэдр
  • Как создать сферу
  • Массивы вершин, нормалей и цветов
  • В этом разделе мы научимся создавать трехмерные изображения с помощью функций библиотеки OpenGL, для того чтобы в следующей главе разработать Windows-приложение, которое можно рассматривать как инструмент просмотра результатов научных расчетов. Материал этого раздела позволит вам постепенно войти в курс дела и овладеть очень привлекательной технологией создания и управления трехмерными изображениями. Сначала мы рассмотрим основные возможности библиотеки OpenGL, затем научимся управлять функциями OpenGL на примере простых приложений консольного типа и лишь после этого приступим к разработке Windows-приложения.

    Интерактивное управление положением и ориентацией

    Интерактивное управление положением и ориентацией

    Теперь хочется рассмотреть трехмерный объект с разных сторон. Удобнее это делать с помощью мыши. Документация MSDN содержит весьма скудные сведения относительно aux-функций, но в примерах все же можно найти какую-то информацию. Оказывается для введения реакции на мышиные события надо ввести в main следующие строки и, конечно, написать функции обработки
    auxMouseFunc(AUX_LEFTBUTTON,AUX_MOUSELOC,OnLMouseMove); auxMouseFunc(AUX_RIGHTBUTTON,AUX_MOUSELOC,OnRMouseMove); auxMouseFunc(AUX_LEFTBUTTON,AUX_MOUSEDOWN,OnButtonDown); auxMouseFunc(AUX_RIGHTBUTTON,AUX_MOUSEDOWN,OnButtonDown);

    Обратите внимание, что дих разделяет WM_MOUSEMOVE на две (в общем случае на три) кнопки. Это нам как раз подходит, так как мы хотим левой кнопкой вращать, а правой удалять-приближать (делать zooming) изображение. События отпускания кнопок нам не понадобились по причине того, что обработчики AUX_MOUSELOC (читай WM_MOUSEMOVE) вызываются только в случае, если соответствующие кнопки нажаты. Поэтому не нужно поднимать и отпускать флаг захвата объекта мышью. Именно это нам и нужно. Как легко, когда кто-то все продумал! Мы не делаем различия между нажатиями левой и правой кнопки, так как задача у них общая — запомнить текущие координаты указателя мыши. Вставьте декларации четырех функций, а затем приступим к их созданию. Так как сейчас мы не имеем классов для инкапсуляции переменных состояния мыши, то придется добавить глобальные переменные:

    int giX, giY; // Текущая позиция указателя мыши

    Тела глобальных функций обработки вы должны вставить до того места, в котором они вызываются. Алгоритм изменения параметров gdAngleX, gdAngleX и gdTransZ очевиден, но обратите внимание на детали. Например, как добывать координаты курсора мыши. Их присылает система, a AUX хранит их в структуре data, информацию о которой вы можете получить разве что в файле заголовков Glaux.h:

    static void _stdcall 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 , а в функцию OnRMouseMove — вызов printf ("\n%d",x);. Теперь координата курсора мыши будет выводиться в текстовое окно консольного приложения. Повторите опыт с правой кнопкой и убедитесь в том, что при выходе за пределы окна (влево), координата х получает недопустимое значение (>65000). Для устранения дефекта достаточно заменить строки:

    int x = pEvent->data[AUX_MOUSEX];
    int у = pEvent->data[AUX_MOUSEY];

    на

    short x = pEvent->data[AUX_MOUSEX];
    short у = pEvent->data[AUX_MOUSEY];

    в функциях OnLMouseMove и OnRMouseMove. Теперь повторите опыт и убедитесь в том, что, переходя через границу окна, координата х изменяется монотонно и приобретает отрицательные значения. Чтобы быть последовательным, замените тип глобальных данных для хранения текущей позиции курсора мыши. Вместо int giX, giY; вставьте short giX, giY;. Объяснение эффекта мы оставляем читателю в качестве упражнения по информатике.

    Интерполяция цвета

    Интерполяция цвета
    Вы можете запустить новый вариант приложения и убедиться в том, что он работает не хуже предыдущего. Кроме подготовки изображения (вызов DrawScene) мы внутри init включили возможность интерполяции цвета точек при заполнении полигона

    glShadeModel (GL_SMOOTH) ;

    Этого можно было и не делать, так как она включена по умолчанию. Но мы вставили код, с тем чтобы явно обозначить намерение исследовать эту возможность. Для того чтобы интерполяция начала действовать, надо задать разные цвета разным вершинам одного полигона. Уберите завершающий фрагмент функции DrawScene, где приведены коды непосредственного рисования куба, и вставьте вместо него следующий текст:

    glPolygonMode (GL_FRONT_AND_BACK, GL_FILL) ;
    glBegin (GL_QUADS) ;

    //====== Обновляем генератор случайных чисел

    srand (time (0) ) ;

    //====== 6 граней куба

    for (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.) позволяет масштабировать и нормировать компоненты цвета. Запустите и проверьте качество интерполяции цветов.

    Использование списков

    Использование списков
    С кубиком быстро расправляется любой компьютер и видеокарта, а вот с более сложными объектами могут возникнуть затруднения, бороться с которыми можно с помощью нескольких приемов. Один из них — использование заранее откомпилированных списков команд OpenGL. Для иллюстрации этого приема создайте отдельную глобальную функцию:

    void DrawScene()

    {
    //====== Создаем новый список команд OpenGL

    glNewList(I,GL_COMPILE);

    //====== Сюда поместите код, рисующий куб,

    //====== начиная со строки

    static float v[8][3] =

    //====== и заканчивая

    for (int j = 0; j < 4; j++)

    glVertex3fv(v[id[i] [j] ] ) ;
    }

    glEnd() ;
    glEndList () ;
    }

    Список рисующих команд OpenGL ограничивается операторными скобками вида:

    glNewList(I, GL_COMPILE);

    //====== Здесь располагаются команды OpenGL

    glEndList () ;

    Первый параметр glNewList (типа GLuint) идентифицирует список с тем, чтобы разработчик мог одновременно использовать несколько списков и вызывать их в нужные моменты времени по номеру. Вызов нашего (единственного) списка мы будем производить командой glCallList(l);. Команды, расположенные между строками giNewList(l, GL_COMPILE); и glEndListQ;, будут откомпилированы и сохранены в списке номер один. В функции перерисовки их следует просто воспроизвести. Для этого замените существующую версию функции OnDraw на новую:

    void _stdcall OnDraw()
    {

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glMatrixMode (GL_MODELVIEW);

    glLoadldentity ();

    glTranslated(gdTransX, gdTransY, gdTransZ);

    glRotated(gdAngleY, 0.,1.,0.);

    glRotated(gdAngleX, 1. ,0 .,0 .);

    //====== Воспроизводим команды из списка 1

    glCallList (1);

    auxSwapBuffers();
    }

    Вызов DrawScene можно осуществить внутри функции initQ, то есть один раз за время существования программного модуля:

    void 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).



    Как убирать внутренние линии

    Как убирать внутренние линии

    Каждой вершине по умолчанию присваивается булевский признак (флаг) того, что из нее может выходить видимое ребро (линия). Если надо отменить рисование линии, например скрыть тесселяцию вогнутого полигона, то можно снять флаг ребра командой glEdgeFlag(GL_FALSE); для текущей вершины. Затем можно вновь установить его, когда дело дойдет до ребер, которые должны быть видимы — команда glEdgeFlag(GL_TRUE);. Попробуйте самостоятельно вставить флаги ребер в следующем фрагменте программы так, чтобы скрыть линию соединения двух четырехугольников. Попробуйте затем заменить алгоритм так, чтобы изобразить ту же фигуру в виде одного полигона:

    void _stdcall 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 ();
    }

    Класс точки в 3D

    Класс точки в 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();
    }



    Контекст передачи изображения

    Контекст передачи изображения

    Окно OpenGL имеет свой собственный формат пикселов. Необходимым условием ее работы является установка pixel-формата экранной поверхности в контексте устройства HDC, а следовательно, и в окне, с которым он связан. Формат устанавливается один раз, повторная установка недопустима, так как может привести к сбоям в работе подсистемы управления окнами (Windows Manager). После установки формата (вызов SetPixelFormat) следует создать контекст передачи изображения OpenGL, описатель которой имеет тип HGLRC. Контекст передачи (rendering context) создается функцией wglCreateContext с учетом выбранного формата пикселов. Контекст передачи изображения — это связь OpenGL с Windows. Создание этого контекста требует, чтобы обычный контекст существовал и был явно указан в параметре wglCreateContext. Контекст HGLRC использует тот же формат пикселов, что и HDC.

    Несмотря на сходство, эти контексты различны. HDC содержит информацию относящуюся к функциям GDI, a HGLRC — к функциям OpenGL. Поток, вызывающий функции OpenGL, должен предварительно объявить контекст передачи текущим (current). Иначе вызовы не будут иметь эффекта. Уничтожать контекст передачи надо после отсоединения его от потока. Несколько контекстов передачи могут одновременно рисовать в окне OpenGL, но только один из них (тот, который ассоциирован с HDC) может быть текущим или активным в потоке.

    Для описания формата пикселов экранной поверхности в OpenGL используется структура PIXELFORMATDESCRIPTOR. Прежде всего pixel format — это описание цветового режима, действующего в данном окне. Например, если видеокарта может работать в режиме передачи 256 цветов, то для кодирования цвета каждого пиксела в этом режиме необходимо иметь 8 бит памяти. В этом случае говорят о 8-битовой глубине поверхности рисования или окна OpenGL. Иногда в таком случае говорят о 8-битовой глубине цвета. Существуют режимы с 15-битовой глубиной (32 768 цветов), 16-битовой (65 536 цветов), 24-битовой (16 миллионов цветов). Выбор формата зависит от возможностей карты и намерений разработчика. Кроме глубины цвета к pixel-формату относятся такие настройки, как:

  • тип буферизации (одинарная или двойная);
  • схема образования цвета (RGBА или на основе палитры);
  • количество бит для буфера глубины, то есть буфера Z-координат изображения (ось Z считается направленной из глубины экрана к наблюдателю);
  • поддержка регулировки прозрачностью (alfa) и др.
  • Вы можете выбрать один из более чем 20 готовых pixel-форматов или задать произвольную комбинацию параметров и попросить найти ближайшую возможную ее реализацию. Microsoft GDI-реализация OpenGL вносит свои коррективы в возможные варианты реализации pixel-формата, а аппаратная поддержка меняется в зависимости от производителя и может значительно расширить его возможности. Каждое окно OpenGL должно иметь свой собственный pixel-формат.

    По умолчанию Windows не вырезает (в смысле перерисовки) дочерние окна из клиентской области родительского окна, поэтому при создании окнам OpenGL следует задать бит стиля WS_CLIPCHILDREN. В этом случае система не позволяет рисовать родительскому GDI-окну в пределах дочернего окна OpenGL. Несколько окон OpenGL, каждое со своим форматом, могут быть отображены одновременно, поэтому необходимо установить еще один бит стиля WS_CLIPSIBLINGS, чтобы предотвратить рисование в окне соседа. Для окон OpenGL недопустим стиль CS_PARENTDC (CM. MSDN).

    Конвейер передачи OpenGL

    Конвейер передачи OpenGL

    Команды OpenGL претерпевают одинаковый порядок обработки, проходя через последовательность стадий, называемых конвейером обработки OpenGL (processing or rendering pipeline). Схема конвейера приводится во многих источниках, приведем ее и мы (Рисунок 6.1), для того чтобы не отсылать читателя к другим книгам. Ниже следует краткое описание его основных блоков.



    Массив вершин нормалей и цветов

    Массив вершин, нормалей и цветов

    Три команды glEnableClientstate говорят о том, что при формировании изображения будут заданы три массива (вершин, нормалей и цветов), а три команды вида gl* Pointer непосредственно задают адреса этих массивов. Здесь важно правильно задать не только адреса трех массивов, но и шаги перемещения по ним. Так как мы вместо трех массивов пользуемся одним массивом структур из трех полей, то шаг перемещения по всем трем компонентам одинаков и равен sizeof (VERT) — размеру одной переменной типа VERT. Массив вершин (vert типа VERT*) и индексов их обхода (Tria типа TRIA*) создается динамически внутри функции init. Характерно, что после того, как закончилось формирование списка рисующих команд OpenGL, мы можем освободить память, занимаемую массивами, так как вся необходимая информация уже хранится в списке. Формирование массивов производится в функции Sphere, которую еще предстоит разработать.

    Далее по коду Init идет формирование списка рисующих команд. Так как массивы вершин и индексов их обхода при задании треугольников уже сформированы, то список рисующих команд создается с помощью одной команды glDrawElements. Ее параметры указывают:

  • тип геометрических примитивов (GL_TRIANGLES);
  • размер массива индексов, описывающих порядок выбора вершин (gnTria*3);
  • тип переменных, из которых составлен массив индексов (GL_UNSIGNED_INT);
  • адрес начала массива индексов.
  • Команды:

    srandftime(0)); // Подготовка ГСЧ

    gShift = CPoint3D(Rand(gMax), Rand(gMax), Rand(gMax));

    позволяют задать характер вращения сферы. Константа const double gMax = 5.;
    выполняет роль регулятора (ограничителя) степени подвижности сферы. Если вам захочется, чтобы сфера вращалась более резво, то увеличьте эту константу и перекомпилируйте проект.



    Массивы вершин нормалей и цветов

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

  • GIVertexPointer — задает адрес массива координат вершин;
  • GINormalPointer — задает адрес массива нормалей в вершинах;
  • GlColorPointer — задает адрес массива цветов, связанных с вершинами;
  • GlTexCoordPointer — задает адрес массива координат текстуры материала, задаваемой в вершинах;
  • GlEdgeFlagPointer — задает адрес массива флагов видимости линий, исходящих из вершины;
  • GllndexPointer — задает адрес массива цветовых индексов вершин в режиме цветовой палитры, а не RGBA.
  • Другой массив индексов — indices, определяет порядок выбора элементов из этих шести массивов. Но этого мало — надо произвести еще некоторые настройки в машине состояний OpenGL. Для перевода ее в режим использования массивов надо несколько раз вызвать функцию glEnableClientstate. Каждый вызов включает один из шести рассмотренных режимов. Только после этого функция glDrawElements способна эффективно задать сразу все примитивы. Например, вызов:

    glEnableClientState(GL_VERTEX_ARRAY);

    включает режим использования массива координат вершин, а вызов этой же функции с параметром GL_NORMAL_ARRAY включает использование массива нормалей.

    Совместно с командой glDrawElements обычно используют тот способ повышения эффективности отображения примитивов, который мы уже используем. Речь идет о паре функций: glNewList, glEndList. Все команды OpenGL, заданные между вызовами этих двух функций, оптимизируются, компилируются (по выбору) и запоминаются в отдельном нумеруемом списке.

    Обзор возможностей библиотеки 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

    Ограничения Microsoft

    К сожалению, Microsoft-реализация OpenGL имеет ряд ограничений, которые не дают в полной мере использовать всю мощь библиотек. Перечислим те из них, которые приведены в документации MSDN.

  • Нет поддержки более новой и популярной библиотеки GLUT, которая в некотором роде аналогична библиотеке GLAUX. Эта проблема решается с помощью Интернет. Вы достаете glut32.dll, glut32.tib и glut.h, которые затем помещаете в следующие папки: WINNT\System32,...\VC7\Lib\H...VC7\Inctude\GL После этого следует указать компоновщику на необходимость подключения glut32.lib. Это делается вместе с подключением других двух библиотек opengl32.lib и glu32.lib (см. ниже).
  • Изображение OpenGL можно вывести на печать только с помощью метафайлов (списка рисующих команд GDI). При этом надо учитывать специфику, описанную в документации.
  • Нет поддержки стереоскопических изображений.
  • OpenGL и GDI-графику можно совмещать только в окне с одинарной буферизацией.
  • Windows имеет одну системную цветовую палитру, которая применяется ко всему экрану, поэтому окно OpenGL не может иметь собственной аппаратной палитры, но может иметь собственную логическую палитру.
  • Окно OpenGL не поддерживает динамический обмен данными (DDE), обмен с помощью механизма Clipboard и OLE. Однако существуют обходные пути для использования операций с Clipboard
  • Библиотеки классов, такие как Volumizer и Open Inventor, которые обеспечивают более высокий уровень конструирования 3-D графики, не включены в состав Microsoft-реализации OpenGL. Это, на мой взгляд, является очень серьезным недостатком.


  • OpenGL — автомат с конечным числом состояний

    OpenGL — автомат с конечным числом состояний

    OpenGL работает по принципу конечного автомата, то есть автомата, который в каждый момент времени находится в одном из состояний, принадлежащих конечному множеству допустимых значений. В документации вы можете встретить в применении к OpenGL термины state machine (конечный автомат) и assembly line (конвейер). Некоторые команды (вызовы функций OpenGL) переводят автомат в различные состояния или режимы, которые остаются неизменными до тех пор, пока не придет следующая команда изменения состояния. Текущий цвет, как вы видели, является одним из состояний. Другими состояниями являются:

  • узор линий или полигонов (stipple patterns);
  • тип проективных или обзорных преобразований (projection and viewing transformations);
  • режимы рисования полигонов;
  • режимы упаковки пикселов;
  • расположение источников света и свойства материалов.
  • Многие переменные, определяющие состояния, переключаются с помощью функций glEnable (включить) или gioisable (выключить). Каждая переменная состояния или режим имеет значение по умолчанию, и в любой точке программы вы можете узнать текущее состояние. Обычно для этой цели используется одна из 6-ТИ команд: glGetBooleanv, glGetDoublev, glGetFloatv, glGetlntegerv, glGetPointerv или glisEnabled. Выбор зависит от типа данных, которые задают состояние. Некоторые переменные состояния (state variables) заполняются более специфичными командами, например: glGetLight*, glGetError, glGetPolygonStipple. Множество состояний можно сохранить в стеке атрибутов командами glPushAttrib или glPushClientAttrib. Обычно так делают для того, чтобы временно изменить что-то, а затем восстановить состояния с помощью одной из команд: glPopAttrib, glPopClientAttrib.

    Ориентация поверхности

    Ориентация поверхности
    Кроме установки параметров света код функции init содержит довольно много других установок, которые мы осуществляем впервые, поэтому обсудим их более подробно. Возможно, вы помните из курса аналитической геометрии, что некоторые поверхности имеет ориентацию. По умолчанию поверхность любого полигона считается лицевой (FRONT), если вы задали ее обходом вершин против часовой стрелки, и она считается изнаночной (BACK), если направление обхода было обратным. В частности, ориентация поверхности влияет на ориентацию нормали.

    Примечание
    Примечание

    Вы можете реверсировать эту установку, задав режим glfrontFace (GL_CW). По умолчанию действует установка glFrontFace(GL_CCW). Аббревиатура CW означает clockwise (по часовой стрелке), a CCW — counterclockwise (против часовой стрелки). Кстати, вы, вероятно, видели в литературе изображение ленты Мебиуса или бутылки Клейна, поверхности которых односторонние и поэтому не имеют ориентации.
    Команда glEnable (GL_CULL_FACE); включает механизм учета ориентации поверхности полигонов. Она должна сопровождаться одним из флагов, определяющих сторону поверхности, например glCullFace(GL_BACK);. Таким образом, мы сообщаем конвейеру OpenGL, что обратные стороны полигонов можно не учитывать. В этом случае рисование полигонов ускоряется. Мы не собираемся показывать внутреннюю поверхность замкнутой сферы, поэтому эти установки нам вполне подходят.



    Основные этапы

    Основные этапы
    Для того чтобы запомнить основные этапы обработки, повторим ключевые моменты.

  • Основная линия конвейера осуществляет преобразование по схеме: Вершины > Примитивы * Фрагменты > Пикселы.
  • Параллельная линия обработки исходных данных задает непосредственно пикселы.
  • Примитивы, заданные в трехмерном пространстве, преобразуются в двухмерное изображение с помощью растеризации.
  • Каждая точка уже двухмерного изображения характеризуется цветом, глубиной (значением координаты Z) и данными о текстуре. Такая точка вместе с сопутствующей информацией называется фрагментом.
  • Фрагмент изменяет соответствующий ему пиксел в буфере кадра, если он проходит пять тестов.
  • Каждая вершина вместе с характеризующими ее данными обрабатывается конвейером OpenGL независимо и последовательно. Это означает, что каждый примитив будет полностью изображен до того, как выполнится следующая команда.
  • Более подробную функциональную схему конвейера вы можете увидеть в разделе MSDN: Platform SDK/OpenGL/Overview/Introduction to OpenGL/OpenGL Processing Pipeline.

    Перспективная проекция

    Перспективная проекция
    В ортографической проекции .(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);

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

  • Сначала с помощью команды glMatrixMode (GL_MODELVIEW); матрица моделирования выбирается в качестве текущей. Обычно при этом она сразу инициализируется единичной матрицей (команда glLoadldentity).
  • После этого текущая (единичная) матрица последовательно домножается справа на матрицы преобразования системы координат, которые формируются с помощью команд glTranslate* (сдвиги), glRotate* (вращения) или glScale* (растяжения-сжатия).
  • Наконец, команды glVertex* генерируют вершины примитивов, то есть координатные векторы точек трехмерного пространства. Векторы умножаются (справа) на текущую матрицу моделирования и тем самым претерпевают такие преобразования, чтобы соответствовать желаемому местоположению и размерам в сцене OpenGL.
  • Предположим, например, что текущая (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


    Если после этого дать команду glVertexSd (x, у, z); то координаты точки (х, у, z) преобразуются по правилам умножения матрицы на вектор:

    1 0 0|| dx || x| |x +dx|
    0 1 0|| dy || y| = |y +dy|
    0 0 1|| dz || z| |z +dz|
    0 0 0|| 1 || 1| |1


    Примечание
    Примечание

    Вы должны помнить, что вершины всех примитивов в OpenGL заданы 4-ком-— понентным вектором (х, у, z, w). По умолчанию нормирующий компонент w-1. При работе с двухмерными изображениями мы для всех вершин задаем координату z = 0. Обратите внимание на то, как четвертый компонент w помогает производить преобразования, в нашем случае сдвиг, а команда glTranslate* учитывает координаты сдвигов вдольтрех пространственных осей (dx, dy, dz).

    Команды вращения 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]]);
    }

    glEnd() ;
    glFlush ();
    }

    Запустите и отладьте приложение. Вы должны увидеть совсем плоский квадрат, несмотря на обещанную трехмерность объекта. Пока ничего вразумительного, никакого трехмерного эффекта. Закомментируйте или удалите (или измените на GL_SMOOTH) настройку glShadeModel (GL_FLAT), так как теперь мы хотим интерполировать цвета при изображении полигонов. Это работает при задании разных цветов вершинам. Попробуйте задать всем вершинам разные цвета.
    Попробуйте покрутить изображение, изменяя значения переменных gdAngleX, gdAngleY. Например, вместо нулевых значений, присваиваемых глобальным переменным по умолчанию, задайте:
    double gdAngleX=20, gdAngleY=20;

    Посмотрите в справке смысл всех параметров функции glRotated и опробуйте одновременное вращение вокруг двух осей, задав большее число единиц в качестве параметров. Позже мы автоматизируем процесс сдвигов и вращений, а сейчас, пока мы не умеем реагировать на сообщения мыши, просто измените значение какого-либо угла поворота и запустите музыку. Объясните результаты. Попробуйте отодвинуть изображение, изменив регулировку gdTransZ. Объясните знак смещения.

    Подготовка окна

    Подготовка окна

    Подготовку контекста передачи OpenGL надо рассматривать как некий обязательный ритуал, в котором порядок действий давно определен и без которого нельзя начинать творческую работу по созданию сцены OpenGL. Стоит где-то промахнуться, и вы увидите молчаливый белый экран. Сначала надо подготовить окно так, чтобы вызовы функций OpenGL начали работать. В этой процедуре выделяют следующие шаги:

  • установка стиля окна;
  • обработка сообщения WM_ERASEBACKGROUND и отказ от стирания фона;
  • установка pixel-формата;
  • создание контекста устройства (НОС) и контекста передачи (HGLRC);
  • специфическая обработка сообщения WM_SIZE;
  • обработка сообщения WM_PAINT;
  • освобождение контекстов при закрытии окна.
  • Чтобы использовать функции библиотеки OpenGL в вашем приложении, надо убедиться, что в системном каталоге Windows присутствуют модули OpenGL32.dll и GLU32.dll. Они должны быть там, так как компания Silicon Graphics (авторы пакета OpenGL) постаралась, чтобы поддержка OpenGL на платформе Windows была максимально доступна и достаточно надежна. Однако хочу предупредить, что я встречал системы, в которых контекст передачи (rendering context) OpenGL работает ненадежно — появляются пятна пробелов и задержка перерисовки, если работа идет не в полноэкранном режиме. Если это есть, то должно проявляться при запуске любой программы, использующей OpenGL. Причина, видимо, в драйвере видеопамяти.

    Типы данных
    OpenGL использует свои собственные типы данных, которые должны соответствовать аналогичным типам той платформы, на которой библиотека установлена. В Microsoft-реализации соответствие типов задано в файле заголовков GL.H так, как показано ниже. Эта таблица понадобится вам при анализе примеров и при разработке собственного кода:

    typedef unsigned int GLenum;
    typedef unsigned char GLboolean;
    typedef unsigned int GLbitfield;
    typedef signed char GLbyte;
    typedef short GLshort;
    typedef int GLint;
    typedef int GLsizei;
    typedef unsigned char GLubyte;
    typedef unsigned short GLushort;
    typedef unsigned int GLuint;
    typedef float GLfloat;
    typedef float GLclampf;

    typedef double GLdouble;
    typedef double GLclampd;
    typedef void GLvoid;

    Подготовка сцены

    Подготовка сцены
    Изображение сферы целесообразно создать заранее (в функции 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;
    }



    Подключаемые библиотеки

    Подключаемые библиотеки

    Microsoft-реализация OpenGL включает полный набор команд OpenGL, то есть глобальных функций, входящих в ядро библиотеки OPENGL32.LIB и имеющих префикс gl (например, glLineWidth). Заметьте, что функции из ядра библиотеки имеют множество версий, что позволяет задать желаемый параметр или настройку любым удобным вам способом. Посмотрите справку по функциям из семейства glColor*. Оказывается, что задать текущий цвет можно 32 способами. Например, функция:

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

    Примечание
    Примечание

    Тип GLbyte эквивалентен типу signed char, a GLdouble — типу double. Свои собственные типы используются в целях упрощения переносимости на другие платформы. Список типов OpenGL мы приведем ниже. Четвертый компонент цвета определяет прозрачность цвета, то есть способ смешивания цвета фона с цветом изображения. Некоторые команды OpenGL имеют в конце символ v, который указывает, что ее аргументом должен быть адрес массива (вектора). Вектор в математике — это последовательность чисел (координат), единственным образом задающих элемент векторного пространства. Многие команды имеют несколько версий, позволяя в конечном счете задать вектор разными способами.

    Около двадцати Windows GDI-функций создано специально для работы с OpenGL. Большая часть из них имеет префикс wgl (аббревиатура от Windows GL). Эти функции являются аналогами функций с префиксом glx, которые подключают OpenGL к платформе X window System. Наконец, существует несколько Win32-функций для управления форматом пикселов и двойной буферизацией. Они применимы только для специализированных окон OpenGL.

    Примитивы OpenGL

    Примитивы OpenGL

    Моделью или объектом в OpenGL называется структура в памяти, конструируемая из геометрических примитивов: точек, линий и полигонов, которые, в свою очередь, задаются своими вершинами (vertices). Из этих моделей OpenGL создает изображение в специально подготовленном окне. Процесс создания и демонстрации изображения называется передачей (rendering) изображения OpenGL. Конечным изображением является множество пикселов — мельчайших видимых элементов экранной поверхности. Информация о цвете пикселов размещена в памяти в виде битовых плоскостей (bitplanes). Так называется область памяти, которая содержит только один бит информации обо всех пикселах окна. В совокупности плоскости составляют буфер кадра (framebuffer), который содержит информацию, необходимую для того, чтобы дисплей отобразил все пикселы окна OpenGL.
    OpenGL изображает графические примитивы (точки, сегменты линий или многоугольники), используя при этом множество независимо управляемых режимов (modes). Для задания примитивов, установки режимов и выполнения других операций необходимо вызывать функции OpenGL, или, как принято говорить, давать последовательность команд OpenGL Примитивы задаются своими вершинами, то есть точками трехмерного пространства. Кроме координат с каждой вершиной ассоциируются такие данные, как цвет, направление нормали (перпендикуляра), параметры текстуры и флаги границы (edge flags). Текстурами называются готовые bitmap-изображения, которые накладываются на многоугольники каркаса модели и вносят в нее эффект поверхности реального материала.

    Разбиение сферы на треугольники

    Рисунок 6.4. Разбиение сферы на треугольники

    Разбиение сферы на треугольники Мы будем управлять степенью дискретизации сферы с помощью двух чисел: количества колец (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 отключите. Эти действия производятся так:

  • Поставьте фокус на элемент дерева OG.cpp в окне Solution Explorer и нажмите клавишу Delete. При этом файл будет отключен от проекта, но он останется в папке проекта.
  • Переведите фокус на строку Console того же окна и, вызвав контекстное меню, дайте команду Add New Item.
  • Выберите шаблон C++ File (.срр) и, задав имя файла Sphere.срр, нажмите ОК.
  • Введите в него директивы препроцессора, которые нам понадобятся, а также объявления некоторых констант:

    #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,; // Число пи



    Рекурсивное деление

    Рекурсивное деление

    Добавим возможность дальнейшего деления треугольников и образования фигур, приближающихся к сфере. Осуществление предельного перехода возможно в математике непрерывных величин, но невозможно в дискретной математике и тем более в реализациях ее алгоритмов, где все множества должны быть конечными. Однако мы можем применить конечную рекурсию для дальнейшего деления треугольников. Замените функцию split на ее рекурсивную версию. Вы, конечно, помните, что рекурсивная функция вызывает сама себя до тех пор, пока не выполнится некоторое условие. Здесь условием выхода из цепи рекурсивных вызовов является равенство нулю последнего параметра depth, который определяет текущую глубину рекурсии:

    void 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 и запустите на выполнение.

    Примечание
    Примечание

    Не знаю, как объяснить, но в Visual Studio б этот код почему-то работает, не-— смотря на явный промах, который типичен не только для начинающих программистов. Опытный читатель, конечно же, заметил, что мы создаем новые списки изображений, не уничтожая старые. Такие действия классифицируются как утечка памяти (memory lickage). Для ее устранения вставьте следующий фрагмент в функцию DrawScene перед вызовом glNewList:

    //====== Если существует 1-й список,

    if (gllsList(1))

    //====== то освобождаем память

    glDeleteLists (1,1);

    Разъяснения можно найти в справке по функциям gllsList и glDeleteLists. He ошибитесь при выборе места вставки фрагмента, так как операции с памятью имеют особую важность. Запустите приложение и, нажимая на пробел, наблюдайте за изменением изображения, которое сначала приближается к сфере, затем постепенно возвращает свой первоначальный облик икосаэдра. Периодически нажимайте клавишу N для того, чтобы оценить влияние точного вычисления нормалей.

    Схема конвейера OpenGL

    Рисунок 6.1. Схема конвейера OpenGL

    Схема конвейера 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) и данными о текстуре. Такая точка вместе с сопутствующей информацией называется фрагментом. Фрагмент изменяет соответствующий ему пиксел в буфере кадра, если он проходит пять тестов:

  • Pixel ownership-тест, который проверяет принадлежность контексту, то есть не закрыт ли фрагмент другим окном;
  • Scissor-тест, который проверяет принадлежность вырезаемому прямоугольнику, который задается функцией glScissor;
  • Alpha-тест, который проверяет четвертый компонент цвета — прозрачность фрагмента с помощью функции glAlphaFunc;
  • Stencil-тест, используемый при создании специальных эффектов. Он, например, проверяет, не попал ли фрагмент в промежуток регулярного узора;
  • Depth-buffer-тест, который проверяет, не закрыт ли фрагмент другим фрагментом с меньшей координатой Z.
  • Кроме того, фрагмент претерпевает другие изменения.

  • текстурирование — это генерация текстурного элемента (texel) на основе texture memory;
  • вычисление дымки (fog);
  • смешивание (blending);
  • интерполяция цвета (dithering);
  • логические операции;
  • маскирование с помощью трафарета (bitmask).


  • Ориентация вектора нормали

    Рисунок 6.2. Ориентация вектора нормали

    Ориентация вектора нормали

    Если координаты векторов а и b известны, то координаты нормали вычисляю по следующим формулам. Длина вектора нормали п зависит от длин вектор сомножителей и величины угла между ними:

    Nx=AxBz-AzBy

    Ny=AzBx-AxBz

    Nz=AxBy-AyBx
    Примечание
    Примечание

    Можно потерять много времени на осознание того факта, что не только правление нормали, но и ее модуль влияют на величину освещенности (и та) вершины, так как сопровождающая документация (Help) не содер; явных указаний на это. Отметьте также, что цвета вершин полигона влияю цвета точек заполнения полигона, так как цвета вновь генерируемых то интерполируются, то есть принимают промежуточные значения между з чениями цвета вершин.
    Чтобы нивелировать зависимость цвета вершины от амплитуды нормали, обыч вектор нормали масштабируют (или нормируют), то есть делают его длину р; ной единице, оставляя неизменным направление. С учетом сказанного создал две вспомогательные функции. Первая масштабирует, а вторая вычисляет н< маль к плоскости треугольника. Алгоритм вычисления использует координа двух сторон, прилегающих к текущей вершине треугольника:

    //====Нормирование вектора нормали (или любого другого)

    void Scale(double v[3])

    {

    double d = sqrt(v[0]*v[0]+v[l]*v[l]+v[2]*v[2]);

    if (d == 0.)

    {

    MessageBox(0,"Zero length vector","Error",MB_OK);

    return;
    }

    void getNorm(double vl[3], double v2[3], double out[3])
    {

    //===== Вычисляем координаты вектора нормали

    //====== по формулам векторного произведения

    out[0] = vl[l]*v2[2] - vl[2]*v2[l];
    out[l] = vl[2]*v2(0] - vl[0]*v2[2] ;
    out[2] =vl[0]*v2[l] - vl[l]*v2[0];
    Scale(out);
    }

    Замените функцию DrawScene. В новом варианте мы аккуратно вычисляем и масштабируем нормали в каждом из двадцати треугольников поверхности икосаэдра:

    void DrawScene()

    {

    static double

    angle - 3. * atanfl.)/2.5, V = cos(angle), W = sin(angle),

    v[12] [3] = {

    {-V,0.,W}, {V,0.,W}, {-V,0.,-W},

    {V,0.,-W}, {0.,W,V}, {0.,W,-V},

    {0.,-W,V}, {0. ,-W,-V}, {W,V, 0.},

    {-W,V,0.}, {W,-V,0.}, {-W,-V,0.}

    };

    static GLuint id[20][3] = {

    (0,1, 4), {0,4, 9}, (9,4, 5), (4,8, 5}, (4,1,8),

    (8,1,10), (8,10,3), (5,8, 3), (5,3, 2), (2,3,7),

    (7,3,10), (7,10,6), (7,6,11), (11,6,0), (0,6,1),

    (6,10,1), (9,11,0), (9,2,11), (9,5, 2), (7,11,2) 1;

    glNewList(l,GL_COMPILE); glColorSd (1., 0.4, 1.) ;
    glBegin(GLJTRIANGLES);

    for (int i = 0; i < 20; i++)
    {

    double dl[3], d2[3], norm[3];

    for (int j = 0; j < 3; j++)

    {

    dl[j] =v[id[i][0]] [j] -v[id[i][l]J [j];
    d2[j] =v[id[i][l]] [j] -v[id[i][2J] [j];

    }

    //====== Вычисление и масштабирование нормали

    getNorm(dl, d2, norm);

    glNormal3dv(norm);

    glVertexSdv(v [ id[i] [1]]);

    glVertex3dv(v[id[i] [1] ] glVertex3dv(v[id[i] [2] ]

    glEnd() ;
    }
    glEndList () ;
    }

    Примечание
    Примечание

    Функцию нормировки всех нормалей можно возложить на автомат OpenGL, если включить состояние GL_NORMALIZE, но обычно это ведет к замедлению перерисовки и, как следствие, выполнения приложения, если изображение достаточно сложное. В нашем случае оно просто, и поэтому вы можете проверить действие настройки, если вставите вызов glEnable (GL_NORMALIZE); в функцию Init (до вызова OrawScene) и временно выключите вызов Scale(out); производимый в функции getNorm. Затем вернитесь к исходному состоянию.

    Деление треугольника икосаэдра

    Рисунок 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;

    Штриховка линий

    Штриховка линий

    Основные действия разворачиваются в функции перерисовки. Здесь мы рисуем несколько линий, изменяя узор их штриховки. Режим штриховки линий включается командой glEnable (GL_LINE_STIPPLE). Узор штриховки задается параметрами функции glLineStipple. Первый параметр является коэффициентом повторения, а второй определяет сам узор. Он должен быть 16-битовой константой или переменной, последовательность бит которой определяет последовательность фрагментов при растеризации линии. Порядок использования битов возрастающий, то есть нулевой бит используется первым. Каждому пикселу соответствует один бит, что характеризует ситуацию: текущий цвет либо включен (бит равен 1), либо выключен (бит равен 0). Алгоритм станет очевидным, если вы запустите приложение (Ctrl+F5), устраните возможные ошибки и увидите результат. Обратите внимание на различие штриховки в 3-й и 4-й строках. Попытайтесь объяснить различие. Создайте несколько своих собственных узоров штриховых (stippled) линий.

    Штриховка полигонов

    Штриховка полигонов

    Теперь применим штриховку (stipple) к полигонам. Режим штриховки включается и выключается стандартным способом:
    glEnable (GL_POLYGON_STIPPLE) ;
    glDisable (GL_POLYGON_STIPPLE);

    Bitmap-узор (pattern) штриховки надо предварительно подготовить в массиве такой размерности, чтобы заполнить bitmap площадью 32x32 = 1024 пиксела. Размерность массива с узором определяется так: 1024 бита можно разместить в 128 переменных по одному байту. Мы разместим их в 16 строках по 8 байт. Имеем 16 х х 8 х 8 = 1024 бита (или пиксела).

    Массивы объявлены глобально. Адреса массивов с узором подаются на вход функции glPolygonStipple:

    GLubyte gSpade[] = // Узор - пики
    {

    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

    0xlf, 0xff, Oxff, 0xf8, 0xlf, 0x00, 0x00, 0xf8,

    0x01, OxcO, 0x03, 0x80, 0x00, 0x70, 0xOe, 0x00,

    0x00,

    0x20,

    0x04,

    0x00,

    0x00,

    0x30

    , 0x0c,

    0x00,

    0x00,

    0x10,

    0x08,

    0x00,

    0x00,

    0x18

    , 0x18,

    0x00,

    0x07,

    0хс4,

    0x23,

    0xe0,

    0x0f,

    0xf8

    , 0xlf,

    0xf0,

    0x38,

    0xlc,

    0x38,

    0xlc,

    0x30,

    0x00

    , 0x00,

    0x0c,

    0x60,

    0x00,

    0x00,

    0x06,

    0x60,

    0x00

    , 0x00,

    0x06,

    0x60,

    0x00,

    0x00,

    0x06,

    0x60,

    0x00

    , 0x00,

    0x06,

    0x60,

    0x00,

    0x00,

    0x06,

    0x30,

    0x00

    , 0x00,

    0x0c,

    0x30,

    0x00,

    0x00,

    0x0c,

    0x18,

    0x00

    , 0x00,

    0x18,

    0х0е,

    0x00,

    0x00,

    0x70,

    0x03,

    0x00

    , 0x00,

    0xc0,

    0x00,

    OxcO,

    0x03,

    0x00,

    0x00,

    0x70

    , 0x0e,

    0x00,

    0x00,

    0x18,

    0x18,

    0x00,

    0x00,

    0x0c

    , 0x30,

    0x00,

    0x00,

    0x07,

    OxeO,

    0x00,

    0x00,

    0x03

    , 0xc0,

    0x00,

    0x00,

    0x01,

    0x80,

    0x00,

    0x00,

    0x00

    , 0x00,

    0x00

    GLubyte

    gStripU =

    // Другой узор -

    полоса



    0x66,

    0x66,

    0x66,

    0x66,

    0x33,

    0x33

    , 0x33,

    0x33,

    0x66,

    0x66,

    0x66,

    0x66,

    0x33,

    0x33

    , 0x33,

    0x33,

    0x66,

    0x66,

    0x66,

    0x66,

    0x33,

    0x33

    , 0x33,

    0x33,

    0x66,

    0x66,

    0x66,

    0x66,

    0x33,

    0x33

    , 0x33,

    0x33,

    0x66,

    0x66,

    0x66,

    0x66,

    0x33,

    0x33

    , 0x33,

    0x33,

    0x66,

    0x66,

    0x66,

    0x66,

    0x33,

    0x33

    , 0x33,

    0x33,

    0x66,

    0x66,

    0x66,

    0x66,

    0x33,

    0x33

    , 0x33,

    0x33,

    0x66,

    0x66,

    0x66,

    0x66,

    0x33,

    0x33

    , 0x33,

    0x33,

    0x66,

    0x66,

    0x66,

    0x66,

    0x33,

    0x33

    , 0x33,

    0x33,

    0x66,

    0x66,

    0x66,

    0x66,

    0x33,

    0x33

    , 0x33,

    0x33,

    0x66,

    0x66,

    0x66,

    0x66,

    0x33,

    0x33

    , 0x33,

    0x33,

    0x66,

    0x66,

    0x66,

    0x66,

    0x33,

    0x33

    , 0x33,

    0x33,

    0x66,

    0x66,

    0x66,

    0x66,

    0x33,

    0x33

    , 0x33,

    0x33,

    0x66,

    0x66,

    0x66,

    0x66,

    0x33,

    0x33

    , 0x33,

    0x33,

    0x66,

    0x66,

    0x66,

    0x66,

    0x33,

    0x33

    , 0x33,

    0x33,

    0x66,

    0x66,

    0x66,

    0x66,

    0x33,

    0x33

    , 0x33,

    0x33,

    Функцию OnDraw замените целиком. Так же поступайте и дальше. Следите лишь за изменениями в функциях main, OnSize и init, которые понадобятся при радикальной смене режима передачи (rendering). Позже мы перейдем к передаче трехмерной графики, а пока режим тот же — двухмерная графика:

    void _stdcall 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 OnDraw()
    {

    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() ;
    }


    Создание консольного проекта

    Создание консольного проекта
    Для исследования возможностей функций библиотек OpenGL целесообразно создать простой проект консольного типа, в котором для работы с другим (Windows) окном будут использованы функции дополнительной библиотеки OpenGL, описанной в файле GLAUX.LIB. Рассмотрим последовательность шагов для создания нового проекта консольного типа.

  • На странице VS Home Page выберите команду Create New Project и в окне появившегося диалога New Project выберите тип проекта Visual C++ Projects, а в качестве шаблона (в поле Templates) — Managed C++ Empty Project.
  • Задайте имя проекта Console, желаемое местоположение папки с проектом и нажмите ОК.
  • Поставьте фокус на элемент Console в окне Solution Explorer, вызовите контекстное меню и выберите команду Add > Add New Item.
  • В окне диалога Add New Item перейдите в список Templates и выберите строку C++File(.cpp).
  • В поле Name того же диалога задайте имя файла OG.cpp и нажмите кнопку Open.
  • Далее вы будете вводить код в окно редактора Studio.Net (вкладка OG.cpp). Для того чтобы компоновщик подключил все библиотеки OpenGL, произведите настройку проекта.

  • Поставьте фокус на элемент Console в окне Solution Explorer и дайте команду Project > Properties или ее эквивалент View t Property Pages.
  • В окне открывшегося диалога Console Property Pages выберите элемент дерева Linker * Input.
  • Переведите фокус в поле Additional Inputs окна справа и добавьте в конец существующего текста имена файлов с описаниями трех библиотек: OPENGL32.LIB GLU32.LIB GLAUX.LIB. Убедитесь в том, что все имена разделены пробелами и нажмите ОК.
  • В новый пустой файл OG.cpp поместите следующий код приложения, которое для создания Windows-окна пользуется услугами библиотеки GLAUX.LIB. Для этого необходимо к проекту консольного типа подключить файл Windows.h1:
    #include
    #include

    //====== Подключение заголовков библиотек OpenGL

    #include

    # include
    #include

    //=====Макроподстановка для изображения одной линии
    #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
    В левой части равенств стоят оконные координаты:

  • (X, Y) — это координаты изображаемого объекта. Мы будем задавать их при формировании граничных точек линий командами glvertex2d;
  • (Хо, Yo) — это координаты левого верхнего угла окна. Они задаются первым и вторым параметрами функции glviewport;
  • сомножители в формуле (width и height) соответствуют третьему и четвертому параметрам (w, h) функции glviewport и равны текущим значениям размеров окна.
  • Как видно из подстановки в формулу, точка с координатами (0,0) попадет в центр окна, а при увеличении ширины или высоты окна (width или height) координаты изображения будут увеличиваться пропорционально. Вызов

    glMatrixMode (GL_PROJECTION);

    определяет в качестве текущей матрицу проецирования, а вызов glLoadldentity делает ее равной единичной матрице. Следующий за этим вызов

    gluOrtho2D (0.0, double(w), 0.0, double(h));

    задает в качестве матрицы преобразования матрицу двухмерной ортографической (или параллельной) проекции. Изображение будет отсекаться конвейером OpenGL, если его детали вылезают из границ, заданных параметрами функции gluOrtho2D.

    Создание сферы

    Создание сферы

    Для иллюстрации рассматриваемых возможностей мы создадим сферу, составленную из треугольников, но при этом не будем отталкиваться от какого-либо правильного многогранника, а используем модель глобуса. Количество и пропорции треугольников будут зависеть от количества геодезических линий на сфере (параллелей и меридианов). Если вы посмотрите на Рисунок 6.4 или представите себе глобус, то согласитесь с тем, что параллели и меридианы разбивают поверхность сферы на множество сферических четырехугольников. Исключение составляют лишь полюса, вокруг которых мы имеем сферические треугольники. Если затем каждый сферический четырехугольник разделить диагональю, то он даст два сферических треугольника.



    Строим икосаэдр

    Строим икосаэдр

    Для иллюстрации работы с массивами вершин создадим более сложный объект — икосаэдр. Это такой дссятистенный дом с острой пятиугольной крышей и таким же полом, но углы пола смещены (повернуты) на л/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.



    Вносим свет

    Вносим свет

    Пока нет освещения, все попытки внести трехмерный реализм обречены на неудачу. Свет отражается по нормали (перпендикуляру) к поверхности. Однако в 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 пор.

    Примечание
    Примечание

    Ассемблерная вставка _asm пор упрощает просмотр значения переменных в окне Variables, так как не дает новых (и отвлекающих) элементов просмотра. Идея использования такого приема принадлежит Марине Полубенцевой, с которой мы сотрудничаем в Microsoft Authorized Education Center при ФПК СПбГТУ (www.Avalon.ru). В книге использовано еще несколько идей и технологических приемов, автором которых является Полубенцева.

    Затем нажмите F5 (Go). Когда выполнение дойдет до точки останова, посмотрите в окно Variables и убедитесь в том, что Lights приняла значение 8. Если хотите, то используйте описанный прием в дальнейшем для выяснения многочисленных параметров и состояний OpenGL. Посмотрите справку по glGet, чтобы получить представление о количестве этих параметров. Теперь уберите отладочный код и включите еще один тумблер в машине состояний OpenGL — учет цвета материала. Для этого вставьте строку:
    glEnable(GL_COLOR_MATERIAL) ;
    в функцию Init и запустите приложение. Обратите внимание на отличие оттенков цвета разных граней. Они определяются OpenGL с учетом направления нормалей. Попробуйте изменить их направление и посмотрите, что получится.

    Выбор способа вычисления нормалей

    Выбор способа вычисления нормалей
    Верните знаки на место и продолжим процесс приближения к шару. Прежде всего восстановим возможность точного вычисления нормалей и дадим пользователю возможность интерактивного выбора между приближенным и точным их вычислением. С этой целью введем глобальную переменную

    //====== Флаг способа вычисления нормалей

    bool 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. Надеюсь, что теперь важность точного вычисления нормалей стала для вас еще более очевидной.

    Cамоучитель по Visual Studio.Net

    Чтение данных

    Чтение данных
    В теле следующей функции ReadData мы создадим файловый диалог, в контексте которого пользователь выбирает файл с новыми данными графика, затем вызовем функцию непосредственного чтения данных (DoRead) и создадим новую сцену на основе прочитанных данных. Попутно мы демонстрируем, как обрабатывать ошибки и работать с файловым диалогом, созданным с помощью функций API. Стандартный диалог открытия файла в этом случае более управляем, и ему можно придать множество сравнительно новых стилей. Стиль OFN_EXPLORER работает только в Windows 2000:

    void 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.



    Диалог по управлению светом

    Диалог по управлению светом
    В окне редактора диалогов (Resource View > Dialog > Контекстное меню > Insert Dialog) создайте окно диалога по управлению светом, которое должно иметь такой вид:



    График по умолчанию

    График по умолчанию
    Пришла пора создать тестовую поверхность у = f (x, z), которую мы будем демонстрировать по умолчанию, то есть до того, как пользователь обратился к файловому диалогу и выбрал файл с данными, которые он хочет отобразить в окне OpenGL Функция Def aultGraphic, коды которой вы должны вставить в файл ChildView,cpp, задает поверхность, описываемую уравнением:


    Yi,j=[3*п*(i-Nz/2)/2*Nz]*SIN[3*п*(j-Nx/2)/2*Nx]

    Здесь п. обозначает количество ячеек сетки вдоль оси Z, а пх — количество ячеек вдоль оси X. Индексы i (0 < i < пz) и j (0 < j < nx) выполняют роль дискретных значений координат (Z, X) и обозначают местоположение текущей ячейки при пробеге по всем ячейкам сетки в порядке, описанном выше. Остальные константы подобраны экспериментально так, чтобы видеть полтора периода изменения гармонической функции.

    Мы собираемся работать с двоичным файлом и хранить в нем информацию в своем формате. Формат опишем словесно: сначала следуют два целых числа m_xsize и m_zSize (размеры сетки), затем последовательность значений функции у = f (х, z) в том же порядке, в котором они были созданы. Перед тем как записать данные в файл, мы поместим их в буфер, то есть временный массив buff, каждый элемент которого имеет тип BYTE, то есть unsigned char. В буфер попадают значения переменных разных типов, что немного усложняет кодирование, но зато упрощает процесс записи и чтения, который может быть выполнен одной командой, так как мы пишем и читаем сразу весь буфер. В процессе размещения данных в буфер используются указатели разных типов, а также преобразование их типов:

    void COGView::DefaultGraphic()
    {

    //====== Размеры сетки узлов

    m xSize = m zSize = 33;

    //====Число ячеек на единицу меньше числа узлов

    UINTnz = m_zSize - 1, nx = m_xSize - 1;

    // Размер файла в байтах для хранения значений функции
    DWORD nSize = m_xSize * m_zSize * sizeof (float) + 2*sizeof (UINT) ;

    //====== Временный буфер для хранения данных

    BYTE *buff = new BYTE[nSize+l] ;

    //====== Показываем на него указателем целого типа

    UINT *p = (UINT*)buff;

    //====== Размещаем данные целого типа

    *р++ = m_xSize;

    *р++ = m_zSize;

    //====== Меняем тип указателя, так как дальше

    //====== собираемся записывать вещественные числа

    float *pf = (float*)?;

    //=== Предварительно вычисляем коэффициенты уравнения
    double fi = atan(l.)*6,

    kx = fi/nx,

    kz = fi/nz;

    //====== В двойном цикле пробега по сетке узлов

    //=== вычисляем и помещаем в буфер данные типа float
    for (UINT i=0; i
    for (UINT j=0; j
    {

    *pf++ = float (sin(kz* (i-nz/2.) ) * sin (kx* (j-nx/2. ) )
    }
    }

    //=== Переменная для того, чтобы узнать сколько
    //=== байт было реально записано в файл DWORD nBytes;

    //=== Создание и открытие файла данных sin.dat

    HANDLE hFile = CreateFile (_T ("sin .dat") , GENERIC_WRITE,

    0, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0)

    //====== Запись в файл всего буфера

    WriteFile (hFile, (LPCVOID) buff, nSize, SnBytes, 0) ;

    //====== Закрываем файл

    CloseHandle (hFile) ;

    //====== Создание динамического массива m_cPoints

    SetGraphPoints (buff, nSize) ;

    //====== Освобождаем временный буфер

    delete [] buff;
    }

    В процессе создания, открытия и записи в файл мы пользуемся API-функциями CreateFile, WriteFile и CloseHandle, которые предоставляют значительно больше возможностей управлять файловых хозяйством, чем, например, методы класса CFile или функции из библиотек stdio.h или iostream.h. Обратитесь к документации, для того чтобы получить представление о них.

    Настройка проекта

    Настройка проекта
  • На странице VS Home Page выберите команду (гиперссылку) Create New Project.
  • В окне диалога New Project выберите уже знакомый вам тип проекта: MFC Application, задайте имя проекта OG и нажмите ОК.
  • В окне мастера MFC Application Wizard выберите вкладку Application Type и задайте такие настройки проекта: Single documents, MFC Standard, Document/View architecture support, Use MFC in a shared DLL.
  • Перейдите на страницу Advanced Features диалога и снимите флажки Printing and print preview, ActiveX Controls, так как мы не будем использовать эти возможности.
  • Нажмите кнопку Finish.
  • Этот тип стартовой заготовки позволяет работать с окном (cocview), которое помещено в клиентскую область окна-рамки (CMainFrame), и создать в этом окне контекст передачи OpenGL. Класс документа нам не понадобится, так как мы собираемся производить все файловые операции самостоятельно, используя свой собственный двоичный формат данных. В связи с этим нам не нужна помощь в сериализации данных, которую предоставляет документ. Для использования функций библиотеки OpenGL надо сообщить компоновщику, чтобы он подключил необходимые библиотеки OpenGL, на сей раз только две.

  • Поставьте фокус на элемент OG в окне Solution Explorer и дайте Команду Project > Properties (или ее эквивалент View > Property Pages).
  • В окне открывшегося диалога OG Property Pages выберите элемент дерева Linker > Input.
  • Переведите фокус в поле Additional Inputs окна справа и добавьте в конец существующего текста имена файлов с описаниями трех библиотек: OPENGL32.LIB GLU32.LIB. Убедитесь в том, что все имена разделены пробелами и нажмите ОК.
  • Чтобы покончить с настройками общего характера, вставьте в конец файла StdAfx.h строки, которые обеспечивают видимость библиотеки OpenGL, а также некоторых ресурсов библиотеки STL:

    #include

    #include

    //=== Подключение заголовков библиотек OpenGL

    #include

    #include

    #include
    using namespace std;


    Параметры освещения

    Параметры освещения
    Установка параметрпв освещения осуществляется подобно тому, как это делалось в предыдущем уроке. Но здесь мы храним все параметры для тога, чтобы можно было управлять освещенностью изображения. Немного позже разработаем диалог, с помощью которого пользователь программы сможет изменять настройки освещения, а сейчас введите коды функции SetLight:

    void COGView::SetLight()
    {

    //====== Обе поверхности изображения участвуют

    //====== при вычислении цвета пикселов

    //====== при учете параметров освещения

    glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, 1);

    //====== Позиция источника освещения

    //====== зависит от размеров объекта

    float fPos[] =
    {

    (m_LightParam[0]-50)*m_fRangeX/100,

    (m_LightParam[l]-50)*m_fRangeY/100,

    (m_LightParam[2]-50)*m_fRangeZ/100,

    l.f
    };
    glLightfv(GL_LIGHTO, GL_POSITION, fPos);

    /1 ====== Интенсивность окружающего освещения

    float f = m_LightParam[3]/100.f;
    float fAmbient[4] = { f, f, f, O.f };
    glLightfv(GL_LIGHTO, GL_AMBIENT, fAmbient);

    //====== Интенсивность рассеянного света

    f = m_LightParam[4]/100.f;

    float fDiffuse[4] = { f, f, f, O.f };

    glLightfv(GL_LIGHTO, GL_DIFFUSE, fDiffuse);

    //====== Интенсивность отраженного света

    f = m_LightParam[5]/100.f;

    float fSpecular[4] = { f, f, f, 0.f };

    glLightfv(GL_LIGHTO, GL_SPECULAR, fSpecular);

    //====== Отражающие свойства материала

    //====== для разных компонентов света

    f = m_LightParam[6]/100.f;

    float fAmbMat[4] = { f, f, f, 0.f };

    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, fAmbMat);

    f = m_LightParam[7]/100.f;

    float fDifMat[4] = { f, f, f, 1.f };

    glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, fDifMat);

    f = m_LightParam[8]/100.f;

    float fSpecMat[4] = { f, f, f, 0.f };

    glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, fSpecMat);

    //====== Блесткость материала

    float fShine = 128 * m_LightParam[9]/100.f;
    glMaterialf(GL FRONT AND BACK, GL SHININESS, fShine);

    //====== Излучение света материалом

    f = m_LightParam[10]/100.f;

    float f Emission [4] = { f , f , f , 0 . f } ;

    glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, fEmission) ;
    }

    Подготовка изображения

    Подготовка изображения
    Разработаем код функции DrawScene, которая готовит и запоминает изображение на основе координат вершин, хранимых в контейнере m_cPoints. Изображение по выбору пользователя формируется либо в виде криволинейных четырехугольников (GL_QUADS), либо в виде полосы связанных четырехугольников (GL_QUAD_STRIP). Точки изображаемой поверхности расположены над регулярной координатной сеткой узлов в плоскости (X, Z). Размерность этой сетки хранится в переменных m_xSize и m_zSize. Несмотря на двухмерный характер сетки, для хранения координат вершин мы используем линейный (одномерный) контейнер m_cPoints, так как это существенно упрощает объявление контейнера и работу с ним. В частности, упрощаются файловые операции. Выбор четырех смежных точек генерируемого примитива (например, GL_QUADS) происходит с помощью четырех индексов (n, i, j, k). Индекс п последовательно пробегает по всем вершинам в порядке слева направо. Более точно алгоритм перебора вершин можно определить так: сначала проходим по сетке узлов вдоль оси X при Z = 0, затем увеличиваем Z и вновь проходим вдоль X и т. д. Индексы i, j, k вычисляются относительно индекса п. В ветви связанных четырехугольников (GL_QUAD_STRIP) работают только два индекса.

    В контейнер m_cPoints данные попадают после того, как они будут прочитаны из файла. Для того чтобы при открытии приложения в его окне уже находился график функции, необходимо заранее создать файл с данными по умолчанию, открыть и прочесть его содержимое. Это будет сделано в коде функций

    DefaultGraphic и SetGraphPoints. Алгоритм функции DrawScene разработан в предположении, что контейнер точек изображаемой поверхности уже существует. Флаг m_bQuad используется для выбора способа создания полигонов: в виде отдельных (GL_QUADS) или связанных (GL_QUAD_STRIP) четырехугольников. Позднее мы введем команду меню для управления этой регулировкой:

    void COGView: : DrawScene ()
    {

    //====== Создание списка рисующих команд

    glNewList (1, GL_COMPILE) ;

    //====== Установка режима заполнения

    //====== внутренних точек полигонов

    glPolygonMode (GL_FRONT_AND_BACK, m_FillMode) ;

    //====== Размеры изображаемого объекта

    UINTnx = m_xSize-l, nz = m_zSize-l;

    //====== Выбор способа создания полигонов

    if (m_bQuad)

    glBegin (GL_QUADS) ;

    //====== Цикл прохода по слоям изображения (ось Z)

    for (UINT z=0, i=0; z
    //====== Связанные полигоны начинаются

    //====== на каждой полосе вновь

    if (!m_bQuad)

    glBegin (GLJ2UAD_STRIP) ;

    //====== Цикл прохода вдоль оси X

    for (UINT x=0; x
    // i, j, k, n — 4 индекса вершин примитива при
    // обходе в направлении против часовой стрелки

    int j = i + m_xSize, // Индекс узла с большим Z

    k = j+1/ // Индекс узла по диагонали

    n = i+1; // Индекс узла справа

    //=== Выбор координат 4-х вершин из контейнера
    float

    xi = m_cPoints [i] .x,

    yi = m_cPoints [i] .у,

    zi = m_cPoints [i] . z,

    xj = m_cPoints [ j ] .x,
    yj = m_cPoints [ j ] .y,
    zj = m_cPoints [ j ] . z,

    xk = m_cPoints [k] .x,
    yk = m_cPoints [k] .y,
    zk = m cPoints [k] . z,

    xn = m_cPoints [n] .x,
    yn = m_cPoints [n] .y,
    zn = m_cPoints [n] . z,

    //=== Координаты векторов боковых сторон ах = xi-xn, ay = yi-yn,

    by = yj-yi, bz = zj-zi,

    //====== Вычисление вектора нормали

    vx = ay*bz, vy = -bz*ax, vz = ax*by,

    //====== Модуль нормали

    v = float (sqrt (vx*vx + vy*vy + vz*vz) ) ;

    //====== Нормировка вектора нормали

    vx /= v; vy /= v; vz /= v;

    //====== Задание вектора нормали

    glNorma!3f (vx,vy,vz);

    // Ветвь создания несвязанных четырехугольников

    if (m_bQuad)

    {

    //==== Обход вершин осуществляется

    //==== в направлении против часовой стрелки

    glColorSf (0.2f, 0.8f, l.f);

    glVertexSf (xi, yi, zi) ;

    glColor3f (0.6f, 0.7f, l.f);

    glVertexSf (xj, у j , zj);

    glColorSf (0.7f, 0.9f, l.f);

    glVertexSf (xk, yk, zk) ;

    glColor3f (0.7f, 0.8f, l.f);

    glVertexSf (xn, yn, zn) ;
    }

    else

    //==== Ветвь создания цепочки четырехугольников
    {

    glColor3f (0.9f, 0.9f, l.0f);

    glVertexSf (xn, yn, zn) ;

    glColorSf (0.5f, 0.8f, l.0f);

    glVertexSf (xj, у j , zj);

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

    if (!m_bQuad) glEnd ( ) ; } //====== Закрываем блок команд GL_QUADS

    if (m_bQuad)

    glEnd() ;

    // ====== Закрываем список команд OpenGL

    glEndList() ;
    }

    При анализе кода обратите внимание на тот факт, что вектор нормали вычисляется по упрощенной формуле, так как линии сетки узлов, над которой расположены вершины поверхности, параллельны осям координат (X, Z). В связи с этим равны нулю компоненты az и bх векторов боковых сторон в формуле для нормали (см. раздел «Точное вычисление нормалей» в предыдущем уроке).

    Подготовка окна

    Подготовка окна
    Вы помните, что подготовку контекста передачи OpenGL надо рассматривать как некий обязательный ритуал, в котором порядок действий определен. В этой процедуре выделяют следующие шаги:
  • установка стиля окна;
  • обработка сообщения WM_ERASEBACKGROUND и отказ от стирания фона;
  • установка pixel-формата;
  • создание контекста устройства (HDC) и контекста передачи (HGLRC);
  • специфическая обработка сообщения WM_SIZE;
  • обработка сообщения WM_PAINT;
  • освобождение контекстов при закрытии окна.
  • Как было отмечено ранее, окнам, которые в своей клиентской области используют контекст передачи OpenGL, при создании следует задать биты стиля WS_CLIPCHILDREN и ws_CLiPSiBLiNGS. Сделайте это внутри существующего тела функции PreCreateWindow класса cocview, добавив нужные биты стиля к тем, что устанавливаются в заготовке:

    BOOL COGView::PreCreateWindow(CREATESTRUCT& cs)
    {

    //====== Добавляем биты стиля, нужные OpenGL

    cs.style |= WS_CLIPSIBLINGS | WS_CLIPCHILDREN;

    return CView::PreCreateWindow(cs);
    }

    Вы помните, что окно OpenGL не должно позволять Windows стирать свой фон, так как данная операция сильно тормозит работу конвейера. В связи с этим введите в функцию обработки WM_ERASEBKGND код, сообщающий системе, что сообщение уже обработано:

    BOOL COGView::OnEraseBkgnd(CDC* pDC)
    {

    return TRUE;
    }

    Окно OpenGL имеет свой собственный формат пикселов. Нам следует выбрать и установить подходящий формат экранной поверхности в контексте устройства HDC, а затем создать контекст передачи изображения (HGLRC). Для описания формата пикселов экранной поверхности используется структура PIXELFORMATDESCRIPTOR. Выбор формата зависит от возможностей карты и намерений разработчика. Мы зададим в полях этой структуры такие настройки:

  • глубину цвета — 24;
  • тип буферизации — двойной;
  • схему образования цвета RGBA;
  • количество бит для буфера глубины — 32;
  • поддержку регулировки прозрачностью и другие специфические настройки выключим.
  • В функцию OnCreate введите код подготовки окна OpenGL. Работа здесь ведется со структурой PIXELFORMATDESCRIPTOR. Кроме того, в ней создается контекст m_hRC и устанавливается в качестве текущего:

    int COGView::OnCreate(LPCREATESTROCT IpCreateStruct)
    {

    if (CView::OnCreate(IpCreateStruct) == -1)
    return -1;

    PIXELFORMATDESCRIPTOR pfd = // Описатель формата
    {

    sizeof(PIXELFORMATDESCRIPTOR), // Размер структуры

    1, // Номер версии

    PFD_DRAW_TO_WINDOW | // Поддержка GDI

    PFD_SUPPORT_OPENGL | // Поддержка OpenGL

    PFD_DOUBLEBUFFER, // Двойная буферизация

    PFD_TYPE_RGBA, // Формат RGBA, не палитра

    24, // Количество плоскостей

    //в каждом буфере цвета

    24, 0, // Для компонента Red

    24, 0, // Для компонента Green

    24, 0, // Для компонента Blue

    24, 0, // Для компонента Alpha

    0, // Количество плоскостей

    // буфера Accumulation

    0, // То же для компонента Red

    0, // для компонента Green

    0, // для компонента Blue

    0, // для компонента Alpha

    32, // Глубина 2-буфера

    0, // Глубина буфера Stencil

    0, // Глубина буфера Auxiliary

    0, // Теперь игнорируется

    0, // Количество плоскостей

    0, // Теперь игнорируется

    0, // Цвет прозрачной маски

    0 // Теперь игнорируется };

    //====== Добываем дежурный контекст

    m_hdc = ::GetDC(GetSafeHwnd());

    //====== Просим выбрать ближайший совместимый формат

    int iD = ChoosePixelForraat(m_hdc, spfd);
    if ( !iD )

    {

    MessageBoxC'ChoosePixelFormat: :Error") ;

    return -1;

    }

    //====== Пытаемся установить этот формат

    if ( ISetPixelFormat (m_hdc, iD, Spfd) )
    {

    MessageBox("SetPixelFormat::Error");

    return -1;
    }

    //====== Пытаемся создать контекст передачи OpenGL

    if ( !(m_hRC = wglCreateContext (m_hdc)))

    {

    MessageBox("wglCreateContext::Error");
    return -1;

    }

    //====== Пытаемся выбрать его в качестве текущего

    if ( IwglMakeCurrent (m_hdc, m_hRC))

    {

    MessageBox("wglMakeCurrent::Error");
    return -1;

    //====== Теперь можно посылать команды OpenGL

    glEnable(GL_LIGHTING); // Будет освещение
    //====== Будет только один источник света

    glEnable(GL_LIGHTO);

    //====== Необходимо учитывать глубину (ось Z)

    glEnable(GL_DEPTH_TEST);

    //====== Необходимо учитывать цвет материала поверхности

    glEnable(GL_COLOR_MATERIAL);

    //====== Устанавливаем цвет фона .

    SetBkColor () ;

    //====== Создаем изображение и запоминаем в списке

    DrawScene () ;
    return 0;

    }

    Контекст передачи (rendering context) создается функцией wglCreateContext с учетом выбранного формата пикселов. Так осуществляется связь OpenGL с Windows. Создание контекста требует, чтобы обычный контекст существовал и был явно указан в параметре wglCreateContext. HGLRC использует тот же формат пикселов, что и НОС. Мы должны объявить контекст передачи в качестве текущего (current) и лишь после этого можем делать вызовы команд OpenGL, которые производят включение некоторых тумблеров в машине состояний OpenGL. Вызов функции DrawScene, создающей и запоминающей изображение, завершает обработку сообщения. Таким образом, сцена рассчитывается до того, как приходит сообщение о перерисовке WM_PAINT. Удалять контекст передачи надо после отсоединения его от потока. Это делается в момент, когда закрывается окно представления. Введите в тело заготовки OnDestroy следующие коды:

    void COGView::OnDestroy(void)
    {

    //====== Останавливаем таймер анимации

    KillTimer(1);

    //====== Отсоединяем контекст от потока

    wglMakeCurrent(0, 0); //====== Удаляем контекст

    if (m_hRC)
    {

    wglDeleteContext(m_hRC);

    m_hRC = 0;
    }

    CView::OnDestroy() ;
    }

    Так же как и в консольном проекте OpenGL, обработчик сообщения WM_SIZE должен заниматься установкой прямоугольника просмотра (giviewport) и мы, так же как и раньше, зададим его равным всей клиентской области окна. -Напомним, что конвейер OpenGL использует эту установку для того, чтобы поместить изображение в центр окна и растянуть или сжать его пропорционально размерам окна. Кроме того, в обработке onSize с помощью матрицы проецирования (GL_PROJECTION) задается тип проекции трехмерного изображения на плоское окно. Мы выбираем центральный или перспективный тип проецирования и задаем при этом угол зрения равным m_AngleView. В конструкторе ему было присвоено значение в 45 градусов:

    void COGView::OnSize(UINT nType, int ex, int cy)
    {

    //====== Вызов родительской версии

    CView::OnSize(nType, ex, cy) ;

    //====== Вычисление диспропорций окна

    double dAspect = cx<=cy ? double(cy)/ex : double(ex)/cy;

    glMatrixMode (GL_PROJECTION) ;
    glLoadldentity() ;

    //====== Установка режима перспективной проекции

    gluPerspective (m_AngleView, dAspect, 0.01, 10000.);

    //====== Установка прямоугольника просмотра

    glViewport(0, 0, сх, су);
    }

    Работа с контейнером

    Работа с контейнером

    Для работы с файлом мы пользовались буфером переменных типа BYTE. Для работы с данными в памяти значительно более удобной структурой данных является динамический контейнер. Мы, как вы помните, выбрали для этой цели контейнер, скроенный по шаблону vector. При заказе на его изготовление указали тип данных для хранения в контейнере. Это объекты класса CPointSD (точки трехмерного пространства). Мы пошли по простому пути и храним в файле только один компонент Y из трех координат точек поверхности в 3D. Остальные две координаты (узлов сетки на плоскости X-Z) будем генерировать на регулярной основе. Такой подход оправдан тем, что изображение OpenGL все равно претерпевает нормирующие преобразования, перед тем как попасть на двухмерный экран. Создание контейнера точек производится в теле функции SetGraphPoints, к разработке которой сейчас и приступим.

    На вход функции подается временный буфер (и его размер), в который попали данные из файла. В настоящий момент в буфере находятся данные тестовой поверхности, а потом, при вызове из функции ReadData, в него действительно попадут данные из файла. Выбор данных из буфера происходит аналогично их записи. Здесь мы пользуемся адресной арифметикой, определяемой типом указателя. Так, операция ++ в применении к указателю типа UINT сдвигает его в памяти на sizeof (UINT) байт. Смена типа указателя (на float*) происходит в тот момент, когда выбраны данные о размерах сетки узлов.

    Для надежности сначала проверяем данные из буфера на внутреннюю непротиворечивость в смысле размерностей. Затем мы уничтожаем данные контейнера и генерируем новые на основе содержимого буфера. В процессе генерации трехмерных координат точек их ординаты (Y) масштабируются для того, чтобы график имел пропорции, удобные для просмотра:

    void COGView::SetGraphPoints(BYTE* buff, DWORD nSize)
    {

    //====== Готовимся к расшифровке данных буфера

    //====== Указываем на него указателем целого типа

    UINT *p = (UINT*)buff;

    //=== Выбираем данные целого типа, сдвигая указатель
    m_xSize = *р; m_zSize = *++p;

    //====== Проверка на непротиворечивость

    if (m_xSize<2 || m_zSize<2 ||

    m_xSize*m_zSize*sizeof(float)

    + 2 * sizeof(UINT) != nSize)
    {

    MessageBox (_T ("Данные противоречивы") ) ;
    return;
    }

    //====== Изменяем размер контейнера

    //====== При этом его данные разрушаются

    m_cPoints . resize (m_xSize*m_zSize) ;

    if (m_cPoints .empty () )
    {

    MessageBox (_T ("He возможно разместить данные")

    return;
    }

    //====== Подготовка к циклу пробега по буферу

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

    float x, z,

    //====== Считываем первую ординату

    *pf = (float*) ++р,

    fMinY = *pf,

    fMaxY = *pf,

    right = (m_xSize-l) /2 . f ,

    left = -right,

    read = (m_zSize-l) /2 . f ,

    front = -rear,

    range = (right + rear) /2. f;

    UINTi, j, n;

    //====== Вычисление размаха изображаемого объекта

    m_fRangeY = range;
    m_fRangeX = float (m_xSize) ;
    m_fRangeZ = float (m_zSize) ;

    //====== Величина сдвига вдоль оси Z

    m_zTrans = -1.5f * m_fRangeZ;

    //====== Генерируем координаты сетки (X-Z)

    //====== и совмещаем с ординатами Y из буфера

    for (z=front, i=0, n=0; i {

    for (x=left, j=0; j {

    MinMax (*pf, fMinY, fMaxY) ;
    m_cPoints[n] = CPoint3D(x, z, *pf++) ;
    }
    }

    //====== Масштабирование ординат

    float zoom = fMaxY > fMinY ? range/ (fMaxY-fMinY)

    : l.f;

    for (n=0; n {

    m_cPoints [n] . у = zoom * (m_cPoints [n] . у - fMinY) - range/2. f;
    }
    }

    При изменении размеров контейнера методом (resize) все его данные разрушаются. В двойном цикле пробега по узлам сетки мы восстанавливаем (генерируем заново) координаты X и Z всех вершин четырехугольников. В отдельном цикле пробега по всему контейнеру происходит масштабирование ординат (умножение на предварительно вычисленный коэффициент zoom). В используемом алгоритме необходимо искать экстремумы функции у = f (x, z). С этой целью удобно иметь глобальную функцию MinMax, которая корректирует значение минимума или максимума, если входной параметр превышает существующие на сей момент экстремумы. Введите тело этой функции в начало файла реализации оконного класса (ChildView.cpp):

    inline void MinMax (float d, floats Min, float& Max)
    {
    //====== Корректируем переданные по ссылке параметры

    if (d > Max)

    Max = d; // Претендент на максимум
    else if (d < Min)

    Min = d; // Претендент на минимум
    }

    Реакции на сообщения Windows

    Реакции на сообщения Windows
    Вспомните, как вы ранее вводили в различные классы реакции на сообщения Windows и повторите эти действия для класса cOGView столько раз, сколько необходимо, чтобы в нем появились стартовые заготовки функций обработки следующих сообщений:

  • WM_CREATE — приложение требует создать окно вызовом CreateEx или Create;
  • WM_DESTROY — окно исчезло с экрана, но не из памяти;
  • WM_ERASEBKGND — фон окна должен быть стерт;
  • WM_LBUTTONDOWN — нажата левая кнопка мыши;
  • WM_LBUTTONUP — отпущена левая кнопка мыши;
  • WM_MOUSEMOVE — курсор мыши перемещается;
  • WM_RBUTTONDOWN — нажата правая кнопка мыши;
  • WM_RBUTTONUP — отпущена правая кнопка мыши;
  • WM_SIZE — изменился размер окна;
  • WM_TIMER — истек квант времени какого-то из таймеров.
  • В конструктор класса вставьте код установки начальных значений переменных:

    COGView::COGView()
    {

    //====== Контекст передачи пока отсутствует

    m_hRC = 0;

    //====== Начальный разворот изображения

    m_AngleX = 35.f;

    m_AngleY = 20.f;

    //====== Угол зрения для матрицы проекции

    m_AngleView = 45.f;

    //====== Начальный цвет фона

    m_BkClr = RGB(0, 0, 96);

    // Начальный режим заполнения внутренних точек полигона
    m_FillMode = GL_FILL;

    //====== Подготовка графика по умолчанию

    DefaultGraphic();

    //====== Начальное смещение относительно центра сцены

    //====== Сдвиг назад на полуторный размер объекта

    m_zTrans = -1.5f*m_fRangeX;
    m_xTrans = m_yTrans = 0.f;

    //== Начальные значения квантов смещения (для анимации)
    m_dx = m_dy = 0.f;

    //====== Мышь не захвачена

    m_bCaptured = false;
    //====== Правая кнопка не была нажата

    m_bRightButton = false;

    //====== Рисуем четырехугольниками

    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
    }

    Реакция на сообщение о перерисовке

    Реакция на сообщение о перерисовке
    В функции перерисовки должна выполняться стандартная последовательность действий, которая стирает back-буфер и буфер глубины, корректирует матрицу моделирования, вызывает из списка команды рисования и по завершении рисования переключает передний и задний буферы. Полностью замените существующий текст функции OnDraw на тот, который приведен ниже: void COGView:: OnDtaw (CDC" pDC]

    glClear
    glMatrixMode(GLjtoDELVIEH) ;

    glLoadldentitylT;

    SetLight() ;

    //=====Формировать

    //===== Переключение буферов SwapBuffera
    }

    Вид той же поверхности но освещенной справа

    Рисунок 7.6. Вид той же поверхности, но освещенной справа

    Вид той же поверхности но освещенной справа
  • убедились, что использование списка команд OpenGL повышает эффективность передачи сложного изображения;
  • применили формулу вычисления нормали к поверхности и убедились в необходимости уделять этой проблеме достаточное внимание;
  • научились управлять освещенностью сцены OpenGL с помощью группы регуляторов;
  • оценили удобство управления группой регуляторов типа slider Control в функции обработки сообщения о прокрутке WM_HSCROLL.


  • Трехмерные графики функций

    Трехмерные графики функций
  • Настройка проекта
  • Реакции на сообщения Windows
  • Подготовка окна
  • Работа с контейнером
  • Чтение данных
  • Управление изображением с помощью мыши
  • Включаем анимацию
  • Ввод новых команд
  • Диалог по управлению светом
  • В этой главе мы разработаем Windows-приложение, которое в контексте OpenGL изображает трехмерный график функции, заданной произвольным массивом чисел. Данные для графика могут быть прочтены из файла, на который указывает пользователь. Кроме этого, пользователь будет иметь возможность перемещать график вдоль трех пространственных осей, вращать его вокруг вертикальной и горизонтальной осей и просматривать как в обычном, так и скелетном режим. Регулируя параметры освещения поверхности, пользователь может добиться наибольшей реалистичности изображения, то есть усилить визуальный эффект трехмерного пространства на плоском экране.
    Графики могут представлять собой результаты расчета какого-либо физического поля, например поверхности равной температуры, давления, скорости, индукции, напряжения и т. д. в части трехмерного пространства, называемой расчетной областью. Пользователь объекта должен заранее подготовить данные и записать их в определенном формате в файл. Объект по команде пользователя считывает данные, нормирует, масштабирует и изображает в своем окне, внедренном в окно приложения-клиента. Пользователь, манипулируя мышью, управляет местоположением и вращением графика, а открыв окно диалога Properties, изменяет другие его атрибуты.

    Управление изображением с помощью мыши

    Управление изображением с помощью мыши
    Итак, мы собираемся управлять ориентацией изображения с помощью левой кнопки мыши. Перемещение курсора мыши при нажатой кнопке должно вращать изображение наиболее естественным образом, то есть горизонтальное перемещение должно происходить вокруг вертикальной оси Y, а вертикальное — вокруг горизонтальной оси X. Если одновременно с мышью нажата клавиша Ctrl, то мы будем перемещать (транслировать) изображение вдоль осей X и Y. С помощью правой кнопки будем перемещать изображение вдоль оси Z. Кроме того, с помощью левой кнопки мыши мы дадим возможность придать вращению постоянный характер. Для этого в обработчик WM_LBUTTONUP введем анализ на превышение квантом перемещения (m_dx, m_dy) некоторого порога чувствительности. Если он превышен, то мы запустим таймер, и дальнейшее вращение будем производить с его помощью. Если очередной квант перемещения ниже порога чувствительности, то мы остановим таймер, прекращая вращение. В обработке WM_MOUSEMOVE следует оценивать желаемую скорость вращения, которая является векторной величиной из двух компонентов и должна быть пропорциональна разности двух последовательных координат курсора. Такой алгоритм обеспечивает гибкое и довольно естественное управление ориентацией объекта. Начнем с обработки нажатия левой кнопки. Оно, очевидно, должно всегда останавливать таймер, запоминать факт нажатия кнопки и текущие координаты курсора мыши:

    void COGView: :OnLButtonDown (UINT nFlags, CPoint point)
    {

    //====== Останавливаем таймер

    KillTimer(1);

    //====== Обнуляем кванты перемещения

    m_dx = 0.f; m_dy = 0.f;

    //====== Захватываем сообщения мыши,

    //====== направляя их в свое окно

    SetCapture ();

    //====== Запоминаем факт захвата

    m_bCaptured = true;

    //====== Запоминаем координаты курсора

    m_pt = point;
    }

    При нажатии на правую кнопку необходимо выполнить те же действия, что и при нажатии на левую, но дополнительно надо запомнить сам факт нажатия правой кнопки, с тем чтобы правильно интерпретировать последующие сообщения о перемещении указателя мыши и вместо вращения производить сдвиг вдоль оси Z:

    void COGView::OnRButtonDown(UINT nFlags, CPoint point)
    {

    //====== Запоминаем факт нажатия правой кнопки

    m_bRightButton = true;

    //====== Воспроизводим реакцию на левую кнопку

    OnLButtonDown(nFlags, point);
    }

    В обработчик отпускания левой кнопки мы вводим анализ на необходимость продолжения вращения с помощью таймера. В случае превышения порога чувствительности, запускаем таймер, сообщения от которого будут говорить, что надо продолжать вращение, поддерживая текущее значение скорости:

    void COGView::OnLButtonUp(UINT nFlags, CPoint point)
    {

    //====== Если был захват,

    if (m_bCaptured)

    //=== то анализируем желаемый квант перемещения
    //=== на превышение порога чувствительности
    if (fabs(m_dx) > 0.5f || fabs(m_dy) > 0.5f)

    //=== Включаем режим постоянного вращения

    SetTimer(1,33,0);
    else

    //=== Выключаем режим постоянного вращения

    KillTimer(1);

    //====== Снимаем флаг захвата мыши

    m_bCaptured = false;

    //====== Отпускаем сообщения мыши

    ReleaseCapture();
    }
    }

    Отпускание правой кнопки должно просто отмечать факт прекращения перемещения вдоль оси Z и отпускать сообщения мыши для того, чтобы они работали на другие окна, в том числе и на наше окно-рамку. Если этого не сделать, то станет невозможным использование меню главного окна. Проверьте, если хотите. Для этого достаточно закомментировать вызов функции ReleaseCapture в обеих функциях:

    void COGView::OnRButtonUp(UINT nFlags, CPoint point)
    {

    //====== Правая кнопка отпущена

    m_bRightButton = false;

    //====== Снимаем флаг захвата мыши

    m_bCaptured = false;

    //====== Отпускаем сообщения мыши

    ReleaseCapture();
    }

    Теперь реализуем самую сложную часть алгоритма — реакцию на перемещение курсора. Здесь мы должны оценить желаемую скорость вращения. Она зависит от того, насколько резко пользователь подвинул объект, то есть оценить модуль разности двух последних позиций курсора, В этой же функции надо выделить случай одновременного нажатия служебной клавиши Ctrl Если она нажата, то интерпретация движения мыши при нажатой левой кнопке изменяется. Теперь вместо вращения мы должны сдвигать объект, то есть пропорционально изменять переменные m_xTrans и m_yTrans, которые затем подаются на вход функции glTranslate. Третья ветвь алгоритма обрабатывает движение указателя при нажатой правой кнопке. Здесь необходимо изменять значение переменной m_zTrans, обеспечивая сдвиг объекта вдоль оси Z. Числовые коэффициенты пропорциональности, которые вы видите в коде функции, влияют на чувствительность мыши и подбираются экспериментально. Вы можете изменить их на свой вкус так, чтобы добиться желаемой управляемости изображения:

    void COGView::OnMouseMove(UINT nFlags, CPoint point)
    {

    if (m_bCaptured) // Если был захват,

    {

    // Вычисляем компоненты желаемой скорости вращения

    m_dy = float (point .у - m_pt .у) /40 . f ;
    m_dx = float (point .x - m_pt .x) /40. f ;

    //====== Если одновременно была нажата Ctrl,

    if (nFlags & MK_CONTROL)
    {

    //=== Изменяем коэффициенты сдвига изображения

    m_xTrans += m_dx;

    m_yTrans -= m_dy;
    }
    else
    {

    //====== Если была нажата правая кнопка

    if (m_bRightButton)

    //====== Усредняем величину сдвига

    m_zTrans += (m_dx + m_dy)/2.f;
    else
    {

    //====== Иначе, изменяем углы поворота

    m_AngleX += m_dy;

    m_AngleY += m_dx;
    }
    }

    //=== В любом случае запоминаем новое положение мыши
    m_pt = point; Invalidate (FALSE) ;
    }
    }

    Запустите и проверьте управляемость объекта. Введите коррективы чувствительности на свой вкус. Попробуйте скорректировать эффект влияния поворота вокруг оси X на интерпретацию знака желаемого вращения вокруг оси Y. Здесь можно воспользоваться стеком матриц моделирования. Теперь добавим код в заготовку функции реакции на сообщения таймера с тем, чтобы ввести фиксацию состояния вращения.

    Установка цвета фона

    Установка цвета фона
    Введите вспомогательную функцию, которая позволяет вычислить и изменить цвет фона окна OpenGL. Позже мы введем возможность выбора цвета фона с помощью стандартного диалога Windows по выбору цвета:

    void COGView: :SetBkColor ()
    {

    //====== Расщепление цвета на три компонента

    GLclampf red = GetRValue (m_BkClr) /255 . f ,

    green = GetGValue (m_BkClr) /255. f ,

    blue = GetBValue(m_BkClr) /255. f ;
    //====== Установка цвета фона (стирания) окна

    glClearColor (red, green, blue, 0.f);

    //====== Непосредственное стирание

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) ;
    }

    Вид окна диалога по управлению параметрами света

    Рисунок 7.4. Вид окна диалога по управлению параметрами света

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

    Таблица 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

    Диалоговый класс

    Для управления диалогом следует создать новый класс. Для этого можно воспользоваться контекстным меню, вызванным над формой диалога.

  • Выберите в контекстном меню команду Add Class.
  • В левом окне диалога Add Class раскройте дерево Visual C++, сделайте выбор MFC > MFC Class и нажмите кнопку Open.
  • В окне мастера MFC Class Wizard задайте имя класса CPropDlg, в качестве базового класса выберите CDialog. При этом станет доступным ноле Dialog ID.
  • В это поле введите или выберите из выпадающего списка идентификатор шаблона диалога IDD_PROP и нажмите кнопку Finish.
  • Просмотрите объявление класса 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 для модального режима). Если происходит попытка повторного открытия диалога, то возможны два варианта развития событий:

  • новый диалог не создается, но окно существующего диалога делается активным;
  • команда открытия диалога недоступна, так как ее состояние зависит от значения указателя m_pDlg.
  • Реализуем первый вариант:

    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. и сравните качество генерируемой поверхности.

    Подведем итог. В этой главе мы:

  • научились превращать окно, поддерживаемое классом cview, в окно OpenGL;
  • вновь использовали стандартный контейнер объектов класса GPoint3D, который удобен для хранения вершин изображаемой поверхности;


  • Вид освещенной поверхности в 3D

    Рисунок 7.1. Вид освещенной поверхности в 3D

    Вид освещенной поверхности в 3D

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

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

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



    Вид поверхности при использовании режима GL_QUAD_STRIP

    Рисунок 7.2. Вид поверхности при использовании режима GL_QUAD_STRIP

    Вид поверхности при использовании режима GL_QUAD_STRIP Обработку следующей команды меню мы проведем в том же стиле, за исключением того, что переменная m_FillMode не является булевской, хоть и принимает лишь два значения (GL_FILL и GL_LINE). Из материала предыдущей главы помните, возможен еще одни режим изображения полигонов — GL_POINT. Логику его реализации при желании вы введете самостоятельно, а сейчас введите коды двух функции обработки команды меню:

    void COGView::OnViewFill(void)
    {

    //=== Переключаем режим заполнения четырехугольника
    m_FillMode = m_FillMode==GL_FILL ? GL_LINE : GL__FILL;

    //====== Заново создаем изображение

    DrawScene();

    Invalidate(FALSE);

    UpdateWindow() ;
    }

    void COGView::OnUpdateViewFill(CCmdUI *pCmdUI)
    {

    //====== Вставляем или убираем маркер выбора

    pCmdUI->SetCheck(m_FillMode==GL_FILL) ;
    }

    Запустите и проверьте работу команд меню. Отметьте, что формула учета освещения работает и в случае каркасного изображения примитивов (Рисунок 7.3).



    Вид поверхности созданной в режиме GL_LINE

    Рисунок 7.3. Вид поверхности, созданной в режиме GL_LINE

    Вид поверхности созданной в режиме GL_LINE Для обмена с диалогом по управлению освещением нам понадобятся две вспомогательные функции GetLightParams и SetLightParam. Назначение первой из которых заполнить массив переменных, отражающих текущее состояние параметров освещения сцены OpenGL. Затем этот массив мы передадим в метод диалогового класса для синхронизации движков (sliders) управления. Вторая функция позволяет изменить отдельный параметр и привести его в соответствие с положением движка. Так как мы насчитали 11 параметров, которыми хотим управлять, то придется ввести в окно диалога 11 регуляторов, которым соответствует массив m_LightPaxam из 11 элементов. Массив уже помещен в класс COGView, нам осталось лишь задействовать его:

    void COGView: :GetLightParams (int *pPos)
    {
    //====== Проход по всем регулировкам

    for (int i=0; i
    //====== Заполняем транспортный массив pPos

    pPos[i] = m_LightParam[i] ;

    void COGView: :SetLightParam (short Ip, int nPos)

    { //====== Синхронизируем параметр lp и

    //====== устанавливаем его в положение nPos

    m_LightParam[lp] = nPos;

    //=== Перерисовываем представление с учетом изменений
    Invalidate (FALSE) ;
    }

    Включаем анимацию

    Включаем анимацию
    Реакция на сообщение о том, что истек очередной квант времени в 33 миллисекунды (именно такую установку мы сделали в OnLButtonUp) выглядит очень просто. Увеличиваем углы поворота изображения на те кванты, которые вычислили в функции OnMouseMove и вызываем перерисовку окна. Так как при непрерывном вращении углы постоянно растут, то можно искусственно реализовать естественную их периодичность с циклом в 360 градусов. Однако с этой задачей успешно справляется OpenGL, и вы можете убрать код ограничения углов:

    void COGView: :OnTimer (UINT nIDEvent)
    {
    //====== Если это был наш таймер

    if (nIDEvent==l)
    {

    //====== Увеличиваем углы поворота

    m_AngleX += m_dy;

    m_AngleY += m_dx;

    //====== Ограничители роста углов

    if (m_AngleX > 360)

    m_AngleX -= 360;
    if (m_AngleX <-360)
    m_AngleX += 360;

    if (m_AngleY > 360)

    m_AngleY -=360;
    if (m_AngleY <-360)

    m_AngleY +=360;

    //====== Просим перерисовать окно

    Invalidate(FALSE);
    }
    else

    //=== Каркас приложения обработает другие таймеры
    CView::OnTimer(nIDEvent);
    }

    Запустите и протестируйте приложение. Скорректируйте, если необходимо, коэффициенты чувствительности.

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

    Вспомогательный класс
    Нам вновь, как и в предыдущем уроке, понадобится класс, инкапсулирующий функциональность точки трехмерного пространства CPoint3D. Контейнер объектов этого класса будет хранить вершины изображаемой поверхности. В коде, который приведен ниже, присутствует слегка измененное по сравнению с предыдущим объявление класса CPoint3D, а также объявления новых данных и методов класса cocview. Заодно мы произвели упрощения стартового кода, которые обсуждались в уроке 5. Весь код введите в файл OGView.h вместо существующей в нем заготовки. Файл должен приобрести следующий вид1:

    #pragma once

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

    class CPointSD
    {

    public: //====== Координаты точки

    float x;
    float у;
    float z;

    //====== Набор конструкторов

    CPointSD ()
    {

    х = у - z = 0.f;
    }

    CPoint3D (float cl, float c2, float c3)
    {

    x = cl; z = c2; У = сЗ; ,
    }

    //====== Операция присвоения

    CPoint3DS operator= (const CPointSDS pt)

    x = pt.x; z = pt.z;
    return *this;

    У = pt.y;

    //====== Конструктор копирования

    CPointSD (const CPoint3D& pt)
    {

    *this = pt;

    //=========== Класс окна OpenGL

    class COGView :
    public CView

    {

    protected:

    COGView () ;
    DECLARE_DYNCREATE(COGView)

    public:

    virtual ~COGView();
    virtual void OnDraw(CDC* pDC) ;
    virtual BOOL PreCreateWindow(CREATESTRUCT& cs) ,

    //======= Новые данные класса

    long m_BkClr; //

    int m_LightParara[ll]; //

    HGLRC m_hRC; //

    HDC m_hdc; //

    GLfloat m_AngleX; //

    GLfloat m_AngleY; //

    GLfloat m_AngleView; //

    GLfloat m_fRangeX; //

    GLfloat m_fRangeY; //

    GLfloat m_fRangeZ; //

    GLfloat m_dx; //

    GLfloat m_dy; //

    GLfloat m_xTrans; //

    GLfloat m_yTrans; //

    GLfloat m_zTrans; //

    GLenura m_FillMode; //

    bool m_bCaptured; //

    bool m_bRightButton; //

    bool m_bQuad; //

    CPoint m_pt; //

    UINT m_xSize; //
    UINT m_zSize; //

    //====== Массив вершин поверхности

    vector m_cPoints;

    //====== Новые методы класса

    //=-==== Подготовка изображения

    void DrawScene();

    Цвет фона окна Параметры освещения Контекст OpenGL Контекст Windows Угол поворота вокруг оси X Угол поворота вокруг оси Y Угол перспективы Размер объекта вдоль X Размер объекта вдоль Y Размер объекта вдоль Z Квант смещения вдоль X Квант смещения вдоль Y Смещение вдоль X Смещение вдоль Y Смещение вдоль Z Режим заполнения полигонов Признак захвата мыши Флаг правой кнопки мыши Флаг использования GL_QUAD Текущая позиция мыши Текущий размер окна вдоль X Текущий размер окна вдоль Y

    //====== Создание графика по умолчанию

    void DefaultGraphic();

    //====== Создание массива по данным из буфера

    void SetGraphPoints(BYTE* buff, DWORD nSize);
    //====== Установка параметров освещения

    void SetLight();

    //====== Изменение одного из параметров освещения

    void SetLightParam (short lp, int nPos);

    //====== Определение действующих параметров освещения

    void GetLightParams(int *pPos); //====== Работа с файлом данных

    void ReadData();

    //====== Чтение данных из файла

    bool DoRead(HANDLE hFile);

    //====== Установка Работа с файлом данных

    void SetBkColor();

    DECLARE MESSAGE MAP()


    Ввод новых команд

    Ввод новых команд

    Вы заметили, что до сих пор обходились без каких-либо ресурсов. Мы не учитываем традиционный диалог About, планку меню главного окна, панель инструментов, две таблицы (строк и ускорителей) и два значка, которые присутствовали в каркасе приложения изначально. Дальнейшее развитие потребует ввести новые ресурсы. Главным из них будет диалог, который мы запустим в немодальном режиме и который позволит подробно исследовать влияние параметров освещения на качество изображения. Начинать, как обычно, следует с команд меню. Скорректируйте меню главного окна так, чтобы в нем появились новые команды:

  • Edit > Properties (ID_EDIT_PROPERTIES);
  • Edit > Background (ID_EDIT_BACKGROUND);
  • View > Fill (ID_VIEW_FILL);
  • View > Quad (ID_VIEW_QUAD).
  • Одновременно удалите не используемые нами команды: File > New, File > Open, File > Save, File > Save as, File > Recent File, Edit > Undo, Edit > Cut, Edit > Copy и Edit > Paste.

    Примечание
    Примечание

    Вы, конечно, знаете, что идентификаторы команд можно не задавать. Они генерируются автоматически при перемещении фокуса от вновь созданной команды к любой другой.
    После этого в классе cocview создайте обработчики всех новых команд с именами по умолчанию (их предлагает Studio.Net). При создании реакций на эти команды меню (COGView > Properties > Events) предварительно раскройте все необходимые элементы в дереве Properties t Commands. Одновременно с функциями обработки типа COMMAND создайте (для всех команд, кроме Edit > Background) функции обновления пользовательского интерфейса, то есть функции обработки типа UPDATE_ COMMANDJJI. Они, как вы помните, следят за состоянием команд меню и соответствующих им кнопок панели управления, обновляя интерфейс пользователя. Команды становятся доступными или, наоборот, в зависимости признака, управляемого програмистом.

    В обработчике OnEditBackground мы вызовем стандартный диалог по выбору цвета, сразу открыв обе его страницы (см. флаг CC_FULLOPEN). С помощью этого диалога пользователь сможет изменить цвет фона:

    void COGView::OnEditBackground(void)
    {
    //====== Создаем объект диалогового класса

    CColorDialog dig(m_BkClr); //====== Устанавливаем бит стиля

    dig.m_cc.Flags |= CC_FULLOPEN;

    //====== Запускаем диалог и выбираем результат

    if (cilg.DoModal ()==IDOK)
    {

    m_BkClr = dig.m_cc.rgbResuit;

    //====== Изменяем цвет фона

    SetBkColor();

    Invalidate(FALSE);
    }
    }

    Проверьте результат, запустив приложение и вызвав диалог. При желании создайте глобальный массив с 16 любимыми цветами и присвойте его адрес переменной lpCustColors, которая входит в состав полей структуры m_сс, являющейся членом класса CColorDialog. В этом случае пользователь сможет подобрать и запомнить некоторые цвета.

    В обработчик OnViewQuad введите коды, инвертирующие булевский признак m_bQuad, который мы используем как флаг необходимости рисования отдельными четырехугольниками (GL_QUADS), и заново создают изображение. Если признак инвертирован, то мы рисуем полосами (GL_QUAD_STRIP):

    void COGView::OnViewQuad(void)
    {

    // Инвертируем признак стиля задания четырехугольников

    m_bQuad = ! m_bQuad;

    //====== Заново создаем изображение

    DrawScene (); Invalidate(FALSE); UpdateWindow();
    }

    В обработчик команды обновления интерфейса введите коды, которые обеспечивают появление маркера выбора рядом с командой меню (или залипания кнопки панели управления):

    void COGView::OnUpdateViewQuad(CCmdUI* pCmdUI)
    {
    //====== Вставляем или убираем маркер (пометку)

    pCmdUI->SetCheck(m_bQuad==true);
    }
    Проверьте результат и попробуйте объяснить зубчатые края поверхности (Рисунок 7.2). Не знаю, правильно ли я поступаю, когда по ходу изложения вставляю задачи подобного рода. Но мной движет желание немного приоткрыть дверь в кухню разработчика и показать, что все не так уж просто. Искать ошибки в алгоритме, особенно чужом, является очень кропотливым занятием. Однако совершенно необходимо приобрести этот навык, так как без него невозможна работа в команде, а также восприятие новых технологий, раскрываемых в основном посредством анализа содержательных (чужих) примеров (Samples). Чтобы обнаружить ошибку подобного рода, надо тщательно проанализировать код, в котором создается изображение (ветвь GL_QUAD_STRIP), и понять, что неправильно выбран индекс вершины. Замените строку givertex3f (xn, yn, zn); HaglVertexSf (xi, yi, zi); и вновь проверьте работу приложения. Зубчатость края должна исчезнуть, но в алгоритме, тем не менее, осталась еще небольшая, слабо заметная неточность. Ее обнаружение и исправление я оставляю вам, дорогой читатель.



    Cамоучитель по Visual Studio.Net

    Библиотека типов

    Библиотека типов
    Для того чтобы клиенты, разработанные на других языках программирования, могли управлять объектами сервера, они должны иметь информацию о типах данных, используемых сервером при передаче параметров. Одним из способов получения этой информации является создание сервером библиотеки типов. Возвращаясь к файлам, которые сгенерировал компилятор MIDL, отметим, что он создает еще один (двоичный) TLB-файл (Type Library). После успешной компиляции вы можете обнаружить его в папке Debug. COM использует этот файл для реализации маршалинга, управляемого данными, который происходит на этапе выполнения программы. Двоичный TLB-файл воспринимается клиентом, написанным на одном из СОМ-совместимых языков. Например, его использует программа просмотра объектов Microsoft Excel. Инструмент Studio.Net ClassWizard умеет по информации из библиотеки типов создать классы, которые могут обращаться к свойствам и методам объектов. Программа на Visual Basic осуществляет раннее связывание на основе данных из библиотеки типов. Сведения о библиотеке типов также заносятся в реестр в специальный подраздел TypeLib в разделе HKEY_CLASSES_ROOT.

    Двойственные интерфейсы

    Двойственные интерфейсы
    Технология Automation, ранее известная как OLE Automation, дает совершенно другой способ вызова клиентом методов, экспонируемых сервером, чем тот стандартный для СОМ способ, который мы уже рассмотрели. Вы помните, что он использует таблицу виртуальных указателей vtable на интерфейсы. Automation же использует стандартный СОМ-интерфейс IDispatch для доступа к интерфейсам. Поэтому говорят, что любой объект, поддерживающий IDispatch, реализует Automation. Также говорят о дуальном интерфейсе, имея в виду, что он может быть вызван как с помощью естественного способа (vtable), так и с помощью вычурного способа Automation. Итак, интерфейс IOpenGL предоставляет своим пользователям двойственный (dual) интерфейс.

    Dual Interface понадобился для того, чтобы VBScript-сценарий мог использовать СОМ-объекты, созданные с помощью Visual C++. Клиенты, созданные на языке C++, могут с помощью Querylnterf асе получить адрес интерфейса и прямо вызывать его методы, пользуясь таблицей виртуальных функций (vtable), например:

    p->SomeMethod(i, d);

    В VBScript будут проблемы. Там нет строгого контроля соответствия типов и многие типы C++ ему неизвестны. Интерфейс IDispatch служит посредником в разговоре двух произведений Microsoft. Теперь программа на VBScript может добраться до метода SomeMethod, выполнив длинную цепь вызовов. Сначала она должна получить указатель на интерфейс IDispatch, затем с его помощью (GetiDsOf Names) узнать индекс желаемого метода (типа DISPID — dispatch identifier), на сей раз не 128-битный. После этого она сможет заставить систему выполнить коды метода SomeMethod, но не прямо, а с помощью метода IDispatch: : Invoke, который требует задать 8 параметров, смысл которых может приблизительно соответствовать следующему списку описаний. Последующий текст воспринимайте очень серьезно, так как он взят прямо из справки IDispatch:: invoke:

  • вызовите пятую функцию из vtable, так как IDispatch: : GetIDsOfNames сообщил, что ее имя SomeMethod;
  • возьмите неиспользуемый (пока) параметр;
  • возьмите 32-битный описатель местности (LCID);
  • возьмите флаг DISPATCH_METHOD | DISPATCH_PROPERTYGET, описывающий суть того, что запрашивается у пятой функции;
  • возьмите адрес структуры DISPPARAMS, в которую завернут массив аргументов, массив индексов (DISPID) для них и числа, описывающие размеры массивов;
  • возьмите адрес структуры VARIANT (из 49 полей, правда 47 из них union), в которой пятая функция может возвратить результат вызова, но только если в 4-м параметре (флаге) указано, что результат нужен;
  • возьмите адрес структуры EXCEPINFO, в которую система в случае непредвиденных обстоятельств запишет причину отказа (сбоя);
  • возьмите адрес переменной, в которой вернется индекс первого аргумента в массиве отказов, так как аргументы там хранятся в обратном порядке, а нам нужна ошибка с самым высоким индексом. Но если в HRESULT будет DISP_E_TYPEMISMATCH или DiSP_E_PARAMNOTFOUND, то возвращаемое значение недействительно.
  • (Поток сознания в скобках, по Джойсу или Жванецкому: новые концепции, новые технологии, глубина мыслей, отточенность деталей, настоящая теория должна быть красивой, тупиковая ветвь?, монополисты не только заставляют покупать, но и навязывают свой способ мышления, что бы ты делал без 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.

    Примечание
    Примечание

    Вступив в царство ATL, придется отречься от многих привычек, приобретенных в MFC. Вы уже заметили, что мы теперь вместо char* или CString пользуемся OLESTR, а вместо COLORREF— OLE_COLOR. Это еще не так отвлекает, но вот теперь надо рисовать без помощи привычного класса CDC и вернуться к описателю НОС контекста устройства, которым мы пользовались при разработке традиционного Windows-приложения на основе функций API. Также придется привыкнуть к тому, что описатель HOC hdcDraw упрятан в структуру типа ATL_DRAWINFO, ссылку на которую мы получаем в параметре метода OnDraw класса CComControl.
    Напомню, что вся функциональность класса CComControl унаследована нашим классом COpenGL, который, кроме него, имеет еще 17 родителей. Состав полей структуры ATL_DRAWINFO не будем приводить здесь, чтобы не усугублять головокружение, а вместо этого предложим убедиться в том, что можно влиять на облик СОМ-объекта. Особенностью перерисовки СОМ-объекта является то, что он изображает себя в чужом окне. Поэтому, получив контекст устройства, связанный с этим окном, он должен постараться не рисовать вне пределов прямоугольника, отведенного для него. В Windows существует понятие поврежденной области окна (clip region). Это обычно прямоугольная область, в пределах которой система позволяет приложению рисовать. Если рисующие функции GDI попробуют выйти за границы этой области, то система не отобразит этих изменений. Следующий код интенсивно работает с clip region, поэтому для понимания алгоритма рекомендуем получить справку о функциях GetClipRgn и SelectClipRgn. Введите изменения в уже существующее тело функции OnDraw так, чтобы она приобрела вид:

    HRESULT OnDraw(ATL_DRAWINFO& di)
    {

    //===== Преобразование RECTL в RECT

    RECT& r = *(RECT*)di.prcBounds;

    //===== Запоминаем текущую поврежденную область

    HRGN hRgnOld = 0;

    //== Функция GetClipRgn может возвратить: 0, 1 или -1
    if (GetClipRgn(di.hdcDraw, hRgnOld) != 1) hRgnOld = 0;

    //====== Создание новой области

    HRGN hRgnNew = CreateRectRgn(r.left,r.top, r.right,r.bottom);

    // Оптимистический прогноз (новая область воспринята)
    bool bSelectOldRgn = false;

    //=== Устанавливаем поврежденную область равной г

    if (hRgnNew)

    {

    bSelectOldRgn = SelectClipRgn(di.hdcDraw,hRgnNew) == ERROR;
    }

    //=== Изменяем цвет фона и обрамляем объект

    ::rSelectObject(di.hdcDraw,
    ::CreateSolidBrush(m_clrFillColor)); Rectangle(di.hdcDraw, r.left, r.top,r.right,r.bottom);

    //=== Параметры выравнивания текста и сам текст
    SetTextAlign(di.hdcDraw, TA_CENTER | TA_BASELINE);
    LPCTSTR pszText = _T("ATL 4.0 : OpenGL");
    //=== Вывод текста в центр прямоугольника
    TextOut(di.hdcDraw, (r.left + r.right)/2,

    (r.top + r.bottom)/2,

    pszText,Istrlen(pszText));

    //=== Если был сбой, то устанавливаем старую область
    if (bSelectOldRgn)

    SelectClipRgn(di.hdcDraw, hRgnOld);
    return S_OK;
    }

    В этой реализации функции OnDraw мы намеренно пошли на поводу у схемы, предложенной в заготовке. Структура RECTL, на которую указывает prcBounds, идентична структуре RECT, но при заливке она ведет себя на один пиксел лучше (см. справку). Здесь это никак не используется. Автору фрагмента не хотелось много раз писать выражение di. prcBounds->, поэтому он завел ссылку на объект типа RECTL, приведя ее к типу RECT. Здесь хочется «взять в руки» CRect, cstring и переписать фрагмент заново в более компактной форме, однако если вы попробуете это сделать, то получите сообщения о том, что CRect и cstring — неизвестные сущности. Они из другого царства MFC. Мы можем подключить поддержку MFC, но при этом многое потеряем. Одной из причин создания ATL была неповоротливость объектов на основе MFC в условиях web-страниц. Мы не можем себе этого позволить, так как собираемся работать с трехмерной графикой. Поэтому надо привыкать работать по правилам Win32-API и классов СОМ.

    Тестирование объекта
    Вновь запустите приложение и убедитесь в том, что нам удалось слегка подкрасить объект. Теперь исследуем функциональность, которую получили бесплатно при оформлении заказа у мастера.

  • Поместите курсор мыши внутрь рамки объекта, вызовите контекстное меню и дайте команду OpenGL Class Object. При этом появится диалоговое окно страниц свойств, состоящее из двух станиц (Property Pages).
  • Сдвиньте окно диалога в сторону, чтобы оно не заслоняло внедренный объект. На первой странице диалога с заголовком Color выберите из списка другой цвет и нажмите кнопку Apply. Цвет должен измениться.
  • В выпадающем списке Set of colours выберите строку System colours Windows и вновь попытайтесь изменить цвет объекта. На сей раз произойдет осечка.
  • Попробуем это исправить. Событие, заключающееся в том, что пользователь объекта изменил одно из его стандартных свойств, поддерживаемых страницами не менее стандартного диалога, будет обработано каркасом СОМ-сервера и при этом вызвана функция copenGL: :OnFillColorChanged, код которой мы не трогали. Сейчас там есть только одна строка:

    ATLTRACE(_T ("OnFillColorChanged\n"));

    которая в режиме отладки (F5) выводит в окно Debug Studio.Net текстовое сообщение. Внесите в тело этой функции изменения:

    void OnFillColorChanged()
    {
    //====== Если выбран системный цвет,

    if (m_clrFillColor & 0x80000000)

    //====== то выбираем его по индексу

    m_clrFillColor=::GetSysColor(m_clrFillColor & Oxlf); ATLTRACE(_T("OnFillColorChanged\n"));
    }

    Признаком выбора системного цвета является единица в старшем разряде m_clrFillColor. В этом случае цвет задан не тремя байтами (red, green, blue), a индексом в таблице системных цветов (см. справку по GetSysColor). Выделяя этот случай, мы выбираем системный цвет с помощью API-функции GetSysColor. Заодно подправим функцию перерисовки, чтобы убедиться, что объект нам подчиняется и мы умеем убирать лишний код:

    HRESULT OnDraw(ATL_DRAWINFO& di)
    {

    //====== Не будем преобразовывать в RECT

    LPCRECTL р = di.prcBounds;

    //====== Цвет подложки текста

    ::SetBkColor(di.hdcDraw,m_clrFillColor) ;

    //====== Инвертируем цвет текста

    ::SetTextColor(di.hdcDraw, ~m_clrFillColor & Oxffffff);
    //====== Цвет фона

    ::SelectObject(di.hdcDraw,
    ::CreateSolidBrush(m_clrFillColor));

    Rectangle(di.hdcDraw, p->left, p->top, p->right, p->bottom);

    SetTextAlign(di.hdcDraw, TA_CENTER | TA_BASELINE);
    LPCTSTR pszText = _T("ATL 4.0 : OpenGL");
    TextOut(di.hdcDraw, (p->left + p->right)/2,
    (p->top + p->bottom)/2,
    pszText, Istrlen(pszText)
    };
    return S_OK;
    }

    Запустите и убедитесь, что системные цвета выбираются корректно, а перерисовка при изменении размеров объекта не нарушает заданных границ. Некоторые проблемы возникают при инвертировании цвета фона, если он близок к нейтральному (128, 128, 128). В качестве упражнения решите эту проблему самостоятельно.

    Фабрика классов

    Фабрика классов

    Логика функционирования нашего проекта (типа клиент-сервер ) вырождена, то есть излишне упрощена, так как мы хотели показать лишь основную нить алгоритма использования СОМ-объектов. Обычно в рамках этого алгоритма присутствует так называемая фабрика классов — специальный класс на стороне сервера, который реализует функциональность уже существующего и зарегистрированного в библиотеке СОМ интерфейса iciassFactory. Фабрики классов — это объекты СОМ создающие другие объекты сервера. Их цель — создать объект определенного типа, который однозначно задан своим CLSID. Каждый СОМ-объект должен в соответствии со стандартом иметь связанную с ним фабрику классов, которая ответственна за его создание. Так, в нашем случае мы должны иметь фабрику классов, способную воспроизводить любое требуемое клиентами количество объектов класса CoSay.
    Интерфейс iciassFactory имеет всего два метода: Createlnstance и LockServer. Первый необходим для того, чтобы динамически создавать произвольное количество объектов тех классов (CLSID), которые живут в доме DLL СОМ-сервера, а второй — для того, чтобы запретить или разрешить системе выгружать сервер из памяти. Это позволяет пользователю гибко управлять необходимыми ресурсами. Если СОМ-объект пока не нужен клиентскому приложению, но вскоре может понадобиться, то, вызвав метод LockServer с параметром TRUE, клиент может запретить выгрузку из памяти DLL-сервера, несмотря на то что счетчик числа пользователей ее объектами равен нулю. Если в течение какого-то времени не предвидится использование СОМ-объектов, то клиент может вызвать метод LockServer с параметром FALSE, разрешив тем самым выгрузку DLL-сервера из памяти.
    Для реализации этой функциональности вновь откройте проект СОМ-сервера My с от и в файл МуСоm.срр добавьте две глобальные переменные:
    //====== Счетчик числа блокировок DLL
    ULONG gLockCount;

    //====== Счетчик числа пользователей СОМ-объектами

    ULONG gObjCount;

    В этот же файл введите новую функцию, которую будет экспортировать наша DLL:

    STDAPI DllCanUnloadNow()
    {

    //====== Если счетчики нулевые, то мы позволяем

    //====== системе выгрузку DLL-сервера

    return !gLockCount && IgObjCount ? S_OK : S_FALSE;
    }

    В конструктор класса coSay добавьте код, увеличивающий счетчик числа пользователей объектом Со Say:

    gObjCount++;

    а в деструктор — уменьшающий:

    gObjCount--;
    Важным шагом, о котором, тем не менее, легко забыть, является своевременная коррекция файла MyCom.def. Вставьте в конец этого файла строку
    DllCanUnloadNow PRIVATE

    которая добавляет в список экспортируемых функций еще один элемент. В файл MyCom. h добавьте декларацию нового класса CoSayFactory, реализующего интерфейс iclassFactory. Отметьте, что он произведен от интерфейса iClassFactory, который, как и положено, имеет родителя I unknown. Вы помните, что на плечи класса ложится бремя реализации всех методов своих предков. По той же причине мы вновь заводим счетчик числа пользователей классом (m_ref):
    //====== Фабрика классов СОМ DLL-сервера

    class CoSayFactory : public IClassFactory
    {

    public:

    CoSayFactory() ;

    virtual ~CoSayFactory() ;

    // lUnknown

    HRESULT _stdcall Querylnterface(REFIID riid,

    void** ppv);

    UbONG _stdcall AddRefO; ULONG _stdcall Release();

    // IClassFactory

    HRESULT _stdcall Createlnstance(LPUNKNOWN pUnk,

    REFIID riid, void** ppv);

    HRESULT _stdcall LockServer(BOOL bLock); private:

    ULONG m_ref; };

    Реализацию тел заявленных методов вставьте в файл МуСоm.срр. Здесь мы вынуждены повторяться, вновь прокручивая логику управления временем жизни объектов СОМ:

    //========== Фабрика классов

    CoSayFactory::CoSayFactory()
    {

    m_ref = 0; gObjCount++;
    }

    CoSayFactory::-CoSayFactory()
    {

    gObjCount--;
    }

    //====== Методы lUnknown

    HRESULT _stdcall CoSayFactory
    ::QueryInterface(REFIID riid, void** ppv)
    {

    *ppv = 0;

    //=== На сей раз обойдемся без шаблона static_cast<>

    if (riid == IID_IUnknown)

    *ppv = (lUnknown*)this;
    else if (riid == IID_IClassFactory)

    *ppv = (IClassFactory*)this;
    else

    return E_NOINTERFACE;

    AddRef();
    return S_OK;
    }

    ULONG _stdcall CoSayFactory:rAddRef()
    {

    return ++m_ref;
    }

    ULONG _stdcall CoSayFactory::Release()
    {

    if (--m_ref==0)
    delete this;

    return m_ref;

    //====== Методы интерфейса IClassFactory

    HRESULT _ stdcall CoSayFactory: :CreateInstance

    (LPUNKNOWN pUnk, REFIID riid, void** ppv)
    {

    // Этот параметр управляет аггрегированием
    // объектов СОМ, которое мы не поддерживаем

    if (pUnk)

    return CLASS_E_NOAGGREGATION;

    //== Создание нового объекта и запрос его интерфейса
    CoSay *pSay = new CoSay;

    HRESULT hr = pSay->Query!nterface (riid, ppv) ;
    if (FAILED (hr))

    delete pSay; return hr;

    //=== Управление счетчиком фиксаций сервера в памяти
    HRESULT _stdcall CoSayFactory::LockServer(BOOL bLock)
    {
    if (bLock) // Если TRUE, то увеличиваем счетчик

    ++gLockCount;
    else // Иначе — уменьшаем

    --gLockCount;
    return S_OK;
    }

    Мы должны также изменить алгоритм функции DllGetciassOb j ect, которая теперь создает объект фабрики классов и запрашивает один из двух возможных интерфейсов (lUnknown, IClassFactory):

    STDAPI DllGetClassObject (REFCLSID rclsid, REFIID riid, LPVOID* ppv)
    {
    if (rclsid != CLSID_CoSay)

    return CLASS_E_CLASSNOTAVAILABLE;

    CoSayFactory *pCF = new CoSayFactory;
    HRESULT hr = pCF->Query!nterface(riid, ppv);

    if (FAILED(hr))
    delete pCF;

    return hr;

    }

    На этом модификация сервера завершается. Дайте команду Build > Rebuild и устраните ошибки, если они имеются. Затем вновь откройте проект клиентского приложения SayClient и внесите изменения в функцию main, которая теперь должна работать с объектами СОМ более изощренным способом. Она должна сначала загрузить СОМ-сервер и запросить адрес его фабрики классов, затем создать с ее помощью объект CoSay, попросив у него адрес интерфейса isay, и лишь после этого можно начать управление объектом. Последовательность освобождения объектов тоже должна быть тщательно выверена. Ниже приведена новая версия файла SayClient.cpp:

    #include "interfaces.h"

    void main()
    {
    (reinitialize (0) ;

    IClassFactory *pCF;

    // Мы зарегистрировали только один класс CoSay,

    // поэтому ищем DLL с его помощью, но при этом

    // создается не объект CoSay, а объект CoSayFactory

    // (см. код функции DllGetClassObject).

    // Поэтому здесь мы просим дать адрес

    // интерфейса IClassFactory

    HRESULT hr = CoGetClassObject(CLSID_CoSay, CLSCTX_INPROC_SERVER,0, IID_IClassFactory,(void**)&pCF);

    if (FAILED(hr))
    {

    MessageBox(0,"Could not Get Class Factory !
    ", "CoGetClassObject", MB_OK);

    CoUninitialize();

    return;
    }

    // Далее мы с помощью фабрики классов

    // создаем объект CoSay и просим его

    // дать нам адрес интерфеса ISay

    ISay *pSay;

    hr = pCF->Create!nstance(0,IID_ISay, (void**)&pSay) ;

    if (FAILED(hr))
    {

    MessageBox(0,"Could not create CoSay and get ISay!
    ", "Createlnstance", MB_OK);

    CoUninitialize ();

    return;
    }

    // Уменьшаем счетчик числа пользователей
    // фабрикой классов pCF->Release();

    //====== Управляем объектом

    pSay->Say();

    BSTR word = SysAllocString(L"Yes, My Lord");

    pSay->SetWord(word);

    SysFreeString(word); pSay->Say();

    //====== Уменьшаем число его пользователей

    pSay->Release();

    SCoUninitialize () ;
    }

    Запустите приложение (Ctrl+F5) и проверьте его работу. Алгоритм проверки остается тем же, что и ранее, но здесь мы должны по логике разработчиков СОМ, радоваться тому, что выполняем большее число правил и стандартов, а также имеем возможность одновременно создавать несколько СОМ-объектов.

    Примечание
    Примечание

    На мой взгляд, не может быть ничего лучшего, чем получить код хорошо продуманного класса C++, который дает вам новую, хорошо документированную функциональность. При этом вы получаете полную свободу в том, как ее использовать, и имеете возможность развивать ее по вашему усмотрению. Использование методов класса предполагает выполнение оговоренных заранее правил игры, так же как и при использовании методов интерфейсов. Но эти правила значительно более естественные, чем правила СОМ. Вы, возможно, возразите, что для внедрения в проект нового класса, сам проект надо строить заново. Двоичный объект СОМ в этом смысле внедрить проще. Но здесь надо учитывать тот факт, что для реализации всех выгод СОМ вам придется разработать универсальный контейнер объектов, который будет способен внедрять СОМ-объекты будущих поколений и управлять ими. Это невозможно сделать, не трогая кода вашего приложения. Разработчик более или менее серьезного проекта постоянно корректирует его, изменяя код того или иного модуля. Он просто обречен на это. На мой взгляд, при реализации новых идей проще использовать исходные коды классов, чем двоичные объекты. Без сомнения, за хорошие коды надо платить, также как и за хорошие СОМ-объекты.

    Файл описания DLL

    Файл описания DLL
    Для успешной работы DLL следует добавить к проекту файл ее описания (DEF-файл). Этот способ является альтернативным и, возможно, более простым, чем использование описателей _declspec(dllexport) для экспортируемых функций.

    DEF-файл сопровождает DLL и содержит список функций, экспортируемых ею. Создайте новый файл MyCom.def и введите в него такие строки:

    LIBRARY "MYCOM.dll"

    EXPORTS DllGetClassObject PRIVATE

    Заметим, что теперь нет необходимости нумеровать экспортируемые функции, как это делалось ранее (например, в рамках Visual Studio 6). Там вы должны были бы задать:

    DllGetClassObject @1 PRIVATE

    При наличии DEF-файла компоновщик создает (кроме основного файла библиотеки MyCom.dll) еще два необходимых файла: MyCom.lib (заголовков экспортируемых функций) и МуСот.ехр (информации об экспортируемых функциях и классах). При отсутствии последних двух файлов система не сможет обратиться к функции DllGetClassObject, а следовательно, и к нашему СОМ-объекту CoSay. Для того чтобы DEF-файл участвовал в процессе сборки DLL, в рамках Visual Studio 6 его достаточно было лишь подключить к проекту. Этого шага, однако, недостаточно в рамках Studio.Net. Надо сделать такую установку:

  • Установите фокус на строке МуСот в окне Solution Explorer и дайте команду View > Propertiy Pages.
  • Раскройте узел Linker > Input в дереве левого окна диалога MyCom Property Pages и введите имя MyCom.def в строку Module Definition File списка свойств.
  • Нажмите кнопку ОК.
  • Следующим шагом вы должны зарегистрировать сервер, то есть внести в реестр Windows записи, которые регистрируют факт существования и местоположение DLL. При работе с ATL это действие будет автоматизировано, но сейчас создайте и подключите к проекту еще один файл MyCom.reg, формат которого соответствует командам регистрации, воспринимаемым редактором реестра RegEdit.exe. При этом вам, вероятна, придется действовать альтернативным способом, описанным выше. По крайней мере в бета-версии Studio.Net, с которой я имею дело, в списке типов добавляемых файлов отсутствует тип REG. В текст, приведенный ниже, вы должны подставить идентификаторы, соответствующие вашей регистрации, а также ваш путь к файлу MyCom.dll:

    REGEDIT

    HKEY_CLASSES_ROOT\MyCom.CoSay\CLSID =
    {9B865820-2FFA-lld5-98B4-OOE0293F01B2}

    HKEY_CLASSES_ROOT\CLSID\
    {9B865820-2FFA-lld5-98B4-OOE0293F01B2}
    = MyCom.CoSay

    HKEY_CLASSES_ROOT\CLSID\
    {9B865820-2FFA-lld5-98B4-OOE0293F01B2}
    \InprocServer32 = D:\MyCom\Debug\MyCom.dll

    Обратите внимание на то, что текст каждой из трех команд не должен разрываться символами перехода на другую строку. В книге мы вынуждены делать переносы, которых не должно быть в вашем файле. Сохраните и закройте файл. Теперь для регистрации сервера и вложенного в него класса СОМ-объекта надо дважды щелкнуть по имени файла MyCom.reg в окне Windows File Manager или Windows Explorer и согласиться с реакцией системы типа «Вы действительно хотите...» После этого соберите проект, дав команду Build > Build. Процесс сборки должен пройти без ошибок. Теперь наш простейший DLL СОМ-сервер зарегистрирован и готов к использованию.

    Сейчас мы займемся разработкой DLL

    Разработка сервера
    Сейчас мы займемся разработкой DLL СОМ-сервера, выполняемого в пространстве процесса другого (клиентского) приложения. Для того чтобы понять, что кроется за этой вывеской, мы для начала создадим минимально-простой СОМ-объект и при этом специально не будем пользоваться какими-либо библиотеками или инструментами Studio.Net.

    Наш объект будет предоставлять миру только один интерфейс isay, инкапсулирующий два метода: Say и SetWord. Первый метод выводит текстовую строку типа BSTR в окно типа MessageBox, а второй — позволяет изменять эту строку. Тип BSTR в Win32 является адресом двухбайтовой Unicode-строки. Его советуют использовать в СОМ-объектах для обеспечения совместимости с клиентскими приложениями, написанными на других языках.

    Я надеюсь, что логика, заложенная в этом простом приложении, поможет вам не терять нить повествования при разработке следующего, более сложного объекта с помощью ATL. Использование ATL и инструментов Studio.Net упрощают разработку СОМ-объектов, но скрывают суть происходящего, вызывая иногда чувство досады и неудовлетворенности. С помощью мастера AppWizard создайте шаблон приложения типа Win32 Dynamic-Link Library (Динамически компонуемая библиотека Win32) под именем МуСот.

  • Дайте команду File > New * Project. В диалоге New Project выберите шаблон Win32 Project под именем МуСот и нажмите ОК.
  • В окне Win32 Application Wizard откройте вкладку Application Settings, установите переключатель Application Type в положение DLL, включите флажок Empty Project и нажмите кнопку Finish.
  • Подключите к проекту новый файл типа C/C++ Header File. Для этого дайте команду Project > Add ' New Item. В диалоге Add New Item выберите шаблон Header File (.h), а в поле Name задайте имя interfaces.h и нажмите кнопку Open
  • Введите в этот файл нижеследующие директивы препроцессора и описание интерфейса ISay.
  • Примечание
    Примечание

    Это же действие можно выполнить более сложным способом, но зато сход-ным с тем, как это делалось в Visual Studio 6. Дайте команду File > New > File, выберите тип файла и нажмите кнопку Open. Кроме этих действий придется записать новый файл в папку с проектом и подключить его. Для этого используется команда Project > Add Existing Item с последующим поиском файла. Альтернативой этому является перетаскивание существующего файла в окне Solution Explorer из папки Resource Files в папку Header Files.

    //=== Эти директивы нужны для того, чтобы не допустить
    //=== повторное подключение файла

    #if !defined(MY_ISAY_INTERFACE)

    #define MY__ISAY_INTERFACE
    #pragma once

    //====== Для того, чтобы были доступны COM API

    #include

    //====== Для того, чтобы был виден lUnknown

    #include

    // Интерфейс ISay мы собираемся зарегистрировать и
    // показать миру. Он, как и положено, происходит от
    // IUnknown и содержит чисто виртуальные функции

    interface ISay : public lUnknown
    {

    //=== 2 метода, которые интерфейс

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

    virtual HRESULT _stdcall Say 0=0;

    virtual HRESULT _stdcall SetWord (BSTR word)=0;
    }

    #endif

    Абстрактный интерфейс не может жить сам по себе. Он должен иметь класс-оболочку (wrapper class), который на деле реализует виртуальные методы Say и SetWord. Этот так называемый ко-класс (класс СОМ-компонента) производится от интерфейса ISay и предоставляет тела всем унаследованным (чисто) виртуальным методам своего родителя. Так как у интерфейса ISay, в свою очередь, имеется родитель (lUnknown), то класс должен также дать реальные тела всем трем методам IUnknown.

    Примечание
    Примечание

    Если вы хотите, чтобы класс реализовывал несколько интерфейсов, то вы должны использовать множественное наследование. Такой подход проповедует ATL (Active Template Library). MFC реализует другой подход к реализации интерфейсов. Он использует вложенные классы. Каждому интерфейсу соответствует новый класс, вложенный в один общий класс СОМ-объекта.
    Для того чтобы быть доступным тем приложениям, которые захотят воспользоваться услугами СОМ-объекта, сам класс тоже должен иметь дом (в виде inproc-сервера DLL). Сейчас, разрабатывая проект типа Win32 DLL, мы строим именно этот дом. С помощью механизма DLL класс будет доступен приложению-клиенту, в адресное пространство процесса которого он загружается. Вы знаете, что DLL загружается в пространство клиента только при необходимости.

    Нам неоднократно понадобятся услуги инструмента Studio.Net под именем GuidGen, поэтому целесообразно ввести в меню Tools (Инструментальные средства) Studio.Net новую команду для его запуска. GuidGen, так же как и UuidGen, умеет генерировать уникальные 128-битовые идентификаторы, но при этом он использует удобный Windows-интерфейс. А идентификаторы понадобятся нам для регистрации сервера и класса CoSay. Для введения новой команды:

  • Дайте команду Tools > External Tools и в окне диалога External Tools нажмите кнопку Add.
  • Введите имя новой команды меню GuidGen, переведите фокус в поле Command и нажмите кнопку справа от нее.
  • С помощью диалога поиска файла, найдите файл Guidgen.exe, который находится в папке .. .\Microsoft Visual Studio.Net\Common7\Tools, и нажмите кнопку Open.
  • Переведите фокус в поле Initial Directory и с помощью кнопки раскрытия выпадающего списка выберите элемент Item Directory.
  • Нажмите OK и теперь с помощью новой команды GuidGen меню Tools вызовите генератор уникальных идентификаторов.
  • Выберите формат DEFINE_GUID и нажмите кнопку Сору, а затем Exit.
  • В окне редактора Studio.Net поместите фокус перед строкой interface ISay и нажмите Ctrl+C. При этом из системного буфера в файл будут помещены три строки кода, которые с точностью до цифр, которые у вас будут другими, имеют такой вид:
  • // {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 } }

    Оно означает, что макрос создает структуру с именем типа GUID, которая служит для хранения уникальных идентификаторов СОМ-объектов, интерфейсов, библиотек типов и других реалий причудливого мира СОМ.

    Интерфейсы — основа СОМтехнологии

    Интерфейсы — основа СОМ-технологии
    Разработчики СОМ не интересуются тем, как устроены компоненты внутри, но озабочены тем, как они представлены снаружи. Каждый компонент или объект СОМ рассматривается как набор свойств (данных) и методов (функций). Важно то, как пользователи СОМ-объектов смогут использовать заложенную в них функциональность. Эта функциональность разбивается на группы семантически связанных виртуальных функций, и каждая такая группа называется интерфейсом. Доступ к каждой функции осуществляется с помощью указателя на нее. В сущности, вся СОМ-технология базируется на использовании таблицы указателей на виртуальные функции (vtable).
    Примечание
    Примечание

    Слово interface (также как и слова object, element) становится перегруженным слишком большим количеством смыслов, поэтому будьте внимательны. Интерфейсы СОМ — это довольно строго определенное понятие, идентичное понятию структуры (частного случая класса) в ООП, но ограниченное соглашениями о принципах его использования.
    Каждый СОМ-компонент может предоставлять клиенту несколько интерфейсов, то есть наборов функций. Стандартное определение интерфейса описывает его как объект, имеющий таблицу указателей на виртуальные функции (vtable). В файле заголовков BaseTyps.h, однако, вы можете увидеть макроподстановку #def ine interface struct, которая показывает, как воспринимает это ключевое слово компилятор языка C++. Для него интерфейс — это структура (частный случай класса), но для разработчиков интерфейс отличается от структуры тем, что в структуре они могут инкапсулировать как данные, так и методы, а интерфейс по договоренности (by convention) должен содержать только методы. Заметим, что компилятор C++ не будет возражать, если вы внутри интерфейса все-таки декларируете какие-то данные.

    Интерфейсы придумали для предоставления (exhibition) клиентам чистой, голой (одной только) функциональности. Существует договоренность называть все интерфейсы начиная с заглавной буквы «I», например lUnknown, ZPropertyNotifySink и т. д. Каждый интерфейс должен жить вечно и поэтому он именуется уникальным 128-битным идентификатором (globally unique identifier), который в соответствии с конвенцией должен начинаться с префикса IID_. Интерфейсы никогда нельзя изменять, усовершенствовать, так как нарушается обратная совместимость. Вместо этого создают новые вечные интерфейсы.

    Примечание
    Примечание

    Это непреложное требование справедливо относят к недостаткам СОМ-техно-логии, так как непрерывное усовершенствование компонентов влечет появление слишком большого числа новых интерфейсов, зарегистрированных в вашем реестре. С проблемой предлагают бороться весьма сомнительным образом — тщательным планированием компонентов. Трудно, если вообще возможно, планировать в наше время (тем более рассчитывать на вечную жизнь СОМ-объекта), когда сами информационные технологии появляются и исчезают, как грибы в дождливый сезон.
    Классы можно производить от интерфейсов (и наоборот), а каждый интерфейс должен в конечном счете происходить от интерфейса lUnknown. Поэтому все интерфейсы и классы, производные от них, наследуют и реализуют функциональность lUnknown. В связи с такой важностью и популярностью этого интерфейса рассмотрим его поближе. Он определяет общую стратегию использования любого объекта СОМ:

    interface lUnknown
    {

    public: virtual HRESULT _stdcall Querylnterface(REFIID riid,

    void **ppvObject) = 0;

    virtual ULONG _stdcall AddRef(void) = 0;
    virtual ULONG _stdcall Release(void) = 0;
    };

    Как видите, «неизвестный» содержит три чисто виртуальные функции и ни одного элемента данных. Каждый новый интерфейс, который создает разработчик, должен иметь среди своих предков I Unknown, а следовательно, он наследует все три указанных метода. Первый метод Querylnterface представляет собой фундаментальный механизм, используемый для получения доступа к желаемой функциональности СОМ-объекта. Он позволяет получить указатель на существующий интерфейс или получить отказ, если интерфейс отсутствует. Первый — входной параметр riid — содержит уникальную ссылку на зарегистрированный идентификатор желаемого интерфейса. Это та уникальная, вечная бирка (клеймо), которую конкретный интерфейс должен носить вечно. Второй — выходной параметр — используется для записи по адресу ppvOb j ect адреса запрошенного интерфейса или нуля в случае отказа. Дважды использованное слово адрес оправдывает количество звездочек в типе void**. Тип возвращаемого значения HRESULT, обманчиво относимый к семейству handle (дескриптор), представляет собой 32-битное иоле данных, в котором кодируются признаки, рассмотренные нами в четвергом уроке.

    Предположим, вы хотите получить указатель на какой-либо произвольный интерфейс 1Му, уже зарегистрированный системой и получивший уникальный идентификатор IID_IMY, с тем чтобы пользоваться предоставляемыми им методами. Тогда следует действовать по одной из общепринятых схем1:

    //====== Указатель на незнакомый объект

    lUnknown *pUnk;
    // Иногда приходит как параметр IМу *рМу;
    // Указатель на желаемый интерфейс

    //====== Запрашиваем его у объекта

    HRESULT hr=pUnk->Query!nterfасе(IID_IMY,(void **)&pMy);

    if (FAILED(hr)) // Макрос, расшифровывающий HRESULT

    {
    //В случае неудачи

    delete pMy; // Освобождаем память

    //====== Возвращаем результат с причиной отказа

    return hr;

    else //В случае успеха

    //====== Используем указатель для вызова методов:

    pMy->SomeMethod();

    pMy->Release(); // Освобождаем интерфейс

    Возможна и другая тактика:

    //====== В случае успеха (определяется макросом)

    if (SUCCEEDED(hr))
    {

    //====== Используем указатель

    }

    else

    {

    //====== Сообщаем о неудаче

    }

    Второй параметр функции Queryinterf асе (указатель на указатель) позволяет возвратить в вызывающую функцию адрес запрашиваемого интерфейса. Примерная схема реализации метода Queryinterf асе (в классе СОМ-объекта, производном от IМу) может иметь такой вид:

    HRESULT _stdcall СМу::Queryinterfасе(REFIID id, void **ppv)
    {

    //=== В *ppv надо записать адрес искомого интерфейса

    //=== Пессимистический прогноз (интерфейс не найден)

    *ppv = 0;

    // Допрашиваем REFIID искомого интерфейса. Если он
    // нам не знаком, то вернем отказ E_NOINTERFACE

    // Если нас не знают, но хотят познакомиться,
    // то возвращаем свой адрес, однако приведенный
    // к типу "неизвестного" родителя
    if (id == IID_IUnknown)

    *ppv = static_cast(this);

    // Если знают, то возвращаем свой адрес приведенный
    // к типу "известного" родителя IМу
    else if (id == IID_IMy)

    *ppv = static_cast(this);

    //===== Иначе возвращаем отказ else

    return E_NOINTERFACE;

    //=== Если вопрос был корректным, то добавляем единицу
    //=== к счетчику наших пользователей
    AddRef();
    return S_OK;
    }

    Методы AddRef и Release управляют временем жизни объектов посредством подсчета ссылок (references) на пользователей интерфейса. В соответствии с общей концепцией объект (или его интерфейс) не может быть выгружен системой из памяти, пока не равен нулю счетчик ссылок на его пользователей. При создании интерфейса в счетчик автоматически заносится единица. Каждое обращение к AddRef увеличивает счетчик на единицу, а каждое обращение к Release — уменьшает. При обнулении счетчика объект уничтожает себя сам. Например, так:

    ULONG СМу::Release()
    {
    //====== Если есть пользователи интерфейса

    if (—m_Ref != 0)

    return m_Ref; // Возвращаем их число
    delete this;
    // Если нет — уходим из жизни,

    // освобождая память
    return 0;
    }

    Вы, наверное, заметили, что появилась переменная m_Ref. Ранее было сказано об отсутствии переменных у интерфейсов. Интерфейсы — это голая функциональность. Но обратите внимание на тот факт, что метод Release принадлежит не интерфейсу 1Му, а классу ему, в котором переменные естественны. Обычно в классе СОМ-объекта и реализуются чисто виртуальные методы всех интерфейсов, в том числе и главного интерфейса zunknown. Класс ему обычно создает разработчик СОМ-объекта и производит его от желаемого интерфейса, например, так:

    class СМу : public IMy

    {

    // Данные и методы класса,

    // в том числе и методы lUnknown
    };

    В свою очередь, интерфейс IMy должен иметь какого-то родителя, может быть, только iUnknown, а может быть одного из его потомков, например:

    interface IMy : IClassFactory
    {

    // Методы интерфейса
    };

    СОМ-объектом считается любой объект, поддерживающий хотя бы lUnknown. Историческое развитие С ОМ-технологий определило многообразие терминов типа: OLE 94, OLE-2, OCX-96, OLE Automation и т. д. Элементы ActiveX принадлежат к той же группе СОМ-объектов. Каждый новый термин из этой серии подразумевает все более высокий уровень предоставляемых интерфейсов. Элементы ActiveX должны как минимум обладать способностью к активизации на месте, поддерживать OLE Automation, допуская чтение и запись своих свойств, а также вызов своих методов.


    Использование макросов COM

    Использование макросов COM

    Разработчики COM рекомендуют для повышения надежности и переносимости компонентов использовать при их разработке множество макроопределений, которые вы также вынуждены будете использовать при разработке проекта на базе ATL. Например, макрос STDMETHODIMP при раскрытии заменяет спецификаторы HRESULT _stdcall. Для того чтобы приобрести навыки использования макросов СОМ, мы применим их в файлах MyCom.h и MyCom.cpp. Сравнивая старую и новую версии этих файлов, вы без труда поймете смысл макроподстановок. В файл MyCom.h ведите коррекцию кодов так, как показано ниже:
    #if !defined(MY_COSAY_HEADER)

    #define MY_COSAY_HEADER
    #pragma once

    #include "MyComTLib_h.h" class CoSay : public ISay

    //====== Класс, реализующий интерфейсы ISay, lUnknown

    public: CoSay (') ;
    virtual -CoSay();

    // lUnknown

    STDMETHODIMP QuerylnterfacefREFIID riid, void** ppv);

    STDMETHODIMP_(ULONG) AddRef();

    STDMETHODIMP_(ULONG) Release();

    // ISay

    STDMETHODIMP Say();

    STDMETHODIMP SetWord (BSTR word);

    private:

    //====== Счетчик числа пользователей классом

    ULONG m_ref;

    //====== Текст, выводимый в окно

    BSTR m_word;
    };

    //====== Фабрика классов СОМ DLL-сервера

    class CoSayFactory : public IClassFactory

    {

    public:

    CoSayFactory();

    virtual ~CoSayFactory();

    // lUnknown

    STDMETHODIMP QueryInterface(REFIID riid, void** ppv) ;

    STDMETHODIMP_(ULONG) AddRef();

    STDMETHODIMP_(ULONG) Release();

    // IClassFactory

    STDMETHODIMP Createlnstance(LPUNKNOWN pUnk,

    REFIID riid, void** ppv);

    STDMETHODIMP LockServer(BOOL bLock);
    private:

    ULONG m_ref; };

    #endif

    Теперь перейдите к файлу MyCom.cpp и произведите замены в соответствии с текстом, приведенным ниже:

    #include "MyComTLib_i.c"

    #include "MyCom.h"

    //====== Произвольный ограничитель длины строк

    #define MAX_LENGTH 128

    //====== Счетчик числа блокировок DLL

    ULONG gLockCount;

    //====== Счетчик числа пользователей СОМ-объектами

    ULONG gObjCount;

    CoSay::CoSay()

    {

    //=== Обнуляем счетчик числа пользователей класса,

    //=== так как интерфейс пока не используется

    m_ref = 0;

    //=== Динамически создаем строку текста по умолчанию
    m_word = SysAllocString(L"This is MyComTLib speaking");
    gObjCount++;
    }

    CoSay::-CoSay()
    {

    //====== при завершении работы освобождаем память

    if (m_word)

    SysFreeString(m_word);

    gObjCount—;
    }

    //====== Реализация методов lUnknown

    STDMETHODIMP CoSay::QueryInterface(REFIID riid, void** ppv)
    {

    // Стандартная логика работы с клиентом

    // Поддерживаем только два интерфейса

    //====== Реализация lUnknown *ppv = 0;

    if (riid==IID_IUnknown)

    *ppv = static_cast(this);
    else if (riid==IID_ISay)

    *ppv = static_cast(this);
    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;
    }



    Как работает DLL

    Как работает DLL

    Вы уже знаете, что созданный и подключенный компоновщиком динамический модуль система интегрирует в пространство другого (клиентского) процесса, загрузив его по определенному базовому адресу. Любая динамически загружаемая библиотека экспортирует функции, которые пишутся в расчете на то, что их будет вызывать клиентское приложение или другая DLL. Глобальная функция DllMain представляет собой точку входа в динамически подключаемую библиотеку. Она является некоторого рода заглушкой (placeholder) для реального, определяемого библиотекой имени функции. Первый параметр DllMain подан операционной системой и представляет собой Windows-описатель DLL. Его можно использовать при вызове функций, требующих этот описатель, например при вызове GetModuleFileName. Второй параметр указывает причину вызова DLL. Он может принимать одно из четырех значений:

  • DLL_PROCESS_ATTACH — указывает на то, что DLL загружается в виртуальное адресное пространство процесса, так как стартовал сам процесс (неявный вызов DLL) или была вызвана функция LoadLibrary (явный вызов DLL).
  • DLL_THREAD_ATTACH — указывает на то, что текущий процесс создает новый поток (thread). В этот момент система вызывает все DLL, которые уже загружены в пространстве процесса, с тем чтобы они учли новый поток в TLS-сло-тах (Thread Local Storage).
  • DLL_THREAD_DETACH — указывает на то, что поток завершается и DLL может освободить динамические ресурсы, связанные с данным потоком, если они были.
  • DLL_PROCESS_DETACH — указывает на то, что DLL выгружается из адресного пространства процесса либо в результате завершения процесса, либо потому, что процесс вызвал функцию FreeLibrary. В этом случае DLL может освободить память (TLS).
  • Если DllMain вернет FALSE или 0, то клиентское приложение завершится с кодом ошибки. Характерно, что стратегия работы с СОМ-объектами сходна со стратегией, используемой при работе с DLL. Последняя заключается в том, что каждый вызов функции LoadLibrary увеличивает на единицу счетчик числа пользователей библиотеки. Вызов функции FreeLibrary уменьшает значение счетчика. Обнаружив, что счетчик числа пользователей равен нулю, операционная система автоматически выгрузит ее. Если после этого вызвать какую-либо экспортируемую DLL функцию, то возникнет исключительная ситуация Access Violation, так как код по указанному адресу уже не отображается на адресное пространство процесса.

    Возвращаясь к коду, созданному мастером ATL Project wizard, отметим, что кроме DllMain, модуль экспортирует еще 4 функции: DllRegisterServer, DllUnregisterServer, DllCanUnloadNow, DllGetClassObject. Полезно открыть, с помощью окна Solution Explorer файл ATLGL.def, который создал и поместил в папку проекта мастер. Этот файл используется компоновщиком при создании lib-файлов и ехр-файлов, содержащих информацию о DLL и экспортируемых ею функциях. Все эти функции имеют тип STDAPI. На самом деле STDAPI — это макроподстановка, заданная в файле заголовков WinNT.h. С помощью этого файла вы можете самостоятельно расшифровать макрос STDAPI. Он разворачивается (expanded) в такой комплексный описатель:

    extern "С" HRESULT _stdcall

    Описатель extern «С» означает, что при вызове функция будет использовать имя в стиле языка С, а не C++, то есть описатель отменяет декорацию имен, производимую компилятором C++ по умолчанию.

    Примечание
    Примечание

    Компилятор C++ использует специальную декорацию имен, для того чтобы отличать overloaded-функции, имеющие одинаковые имена, но разные прото-. типы. Например, вызов: int func(int a, double b); в результате декорации становится: _func@12. Число 12 описывает количество байт, занимаемых списком аргументов. Такая условность называется naming convention (соглашение об именах). Есть и другая конвенция — calling convention (соглашение о связях), которая определяет договоренность о передаче параметров при вызове Win32 API-функций. Описатель _stdcall относится к этой группе. Он определяет: порядок передачи аргументов (справа налево): то, что аргументы передаются по значению (by value), что вызываемая функция должна сама выбирать аргументы из стека и что трансляция регистра символов, верхнего или нижнего, не производится.
    Функция DllCanUnloadNow определяет, используется ли данная DLL в данный момент. Если нет, то вызывающий процесс может безопасно выгрузить ее из памяти. Функция DllGetClassObject с помощью третьего параметра (LPVOID* ppv) возвращает адрес так называемой фабрики классов, которая умеет создавать СОМ-объекты, по известному CLSID — уникальному идентификатору объекта.

    Откройте файл ATLGLJ.c и.убедитесь, что он пуст. Этот файл будет заполнен кодами компилятором MIDL, о котором мы уже говорили ранее. Запустите приложение (Ctrl+F5). Компилятор и компоновщик создадут исполняемый модуль типа DLL, но загрузчик не будет знать в рамках какого процесса (контейнера) следует запустить его на отладку.

    Примечание
    Примечание

    В этот момент Studio.Net запросит имя ехе-файла, то есть модуля или процесса в пространство которого должна быть загружена созданная компоновщиком DLL. Вы можете воспользоваться выпадающим списком для выбора строки Browse, которая даст диалог по выбору файла. Найдите с его помощью стандартный контейнер для отладки элементов ActiveX (tstcon32.exe), поставляемый вместе со Studio.Net по адресу:...\MicrosoftVisualStudio.Net\Common7\Tools и нажмите Open, а затем ОК.
    В рамках тестового контейнера можно отлаживать работу элементов ActiveX, OLE-controls и других СОМ-объектов. Но сейчас наш объект еще не готов к этому, так как мы не создали СОМ-класса, инкапсулирующего желаемые интерфейсы. Поэтому закройте тестовый контейнер, вновь откройте в рамках Studio.Net уже существующий IDL-файл (Interface Description Language file) ATLGLidl и просмотрите коды, описывающие интерфейс, СОМ-класс и библиотеку типов. Вы помните, что этот файл обрабатывается компилятором MIDL, который на его основе создает другие файлы. Откройте файл ATLGM.c и убедитесь, что теперь он не пуст. Его содержимое было создано компилятором MIDL. В данный момент файл ATLGM.c содержит только один идентификатор библиотеки, который регистрируется с помощью макроподстановки MIDL_DEFINE_GUID.

    Как работают СОМсерверы

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

    СОМ-серверы, которые хранятся в DLL-файлах, называются внутризадачными (in-process) серверами. Но они могут быть реализованы и в виде ЕХЕ-файлов. Тогда они называются либо локальными (local) серверами, либо удаленными (remote) серверами. Приложение-клиент и локальный сервер функционируют в отдельных процессах или адресных пространствах в рамках одной машины. Клиент и удаленный сервер функционируют не только в отдельных процессах (адресных пространствах), но и разделены сетевыми каналами связи. И тем и другим необходим коммуникационный мост, чтобы вызывать функции и передавать друг другу данные. Такой мост обеспечивают библиотеки OLE, которые в качестве средства реализации используют механизм RFC (Remote Procedure Call — удаленный вызов процедуры). , Существует еще одна классификация СОМ или OLE-объектов. В рамках MFC и поддерживаемой ею архитектуры документ — представление мы можем создать объекты, которые либо поддерживают связь (linked) с приложением-контейнером, либо внедрены в него (embedded). Некоторые приложения поддерживают как связывание, так и внедрение объектов. Основное различие между двумя типами OLE-объектов заключается в том, что источник данных внедренного (embedded) объекта является частью документа контейнера и хранится вместе с данными контейнера, в то время как данные связанного (linked) объекта хранятся в документе сервера, то есть в файле, созданном и управляемым сервером. Объект контейнера, который связан (linked), хранит лишь информацию, необходимую для связи с документом сервера. Говорят, что объект контейнера хранит связь с документом сервера. Приложение-сервер, поддерживающее связывание, должно уметь копировать свои данные в буфер обмена для выполнения нужд контейнера по копированию объекта. Обычно под внедренным объектом понимается обобщенный объект, независимо от способа общения с ним (linked или embedded).

    В конце этого урока мы (в рамках другой библиотеки — ATL) создадим DLL-сервер, который выполняет роль простейшего элемента ActiveX, внедряемого в окно приложения-клиента. Но сначала подробно рассмотрим, как взаимодействуют клиент и сервер в рамках приложения, использующего «сырые» (raw) функции COM API, с разработки которых и началось движение СОМ.

    Концепция маршалинга

    Концепция маршалинга
    СОМ спроектирован так, чтобы обеспечить прозрачную (transparent) коммуникацию клиента с сервером независимо от того, где они находятся:

  • в пространстве одного процесса,
  • на одном компьютере, но в разных процессах,
  • на разных компьютерах.
  • С точки зрения клиента все СОМ-объекты управляются одинаковым способом — с помощью указателя на интерфейс, который должен действовать в адресном пространстве клиента. Если СОМ-объект находится в этом же пространстве, то вызов метода какого-либо из его интерфейсов осуществляется прямо, без посредников. Если объект расположен вне рамок клиентского процесса, то вызов осуществляется с помощью посредников, называемых заглушками. Их либо автоматически генерирует СОМ, либо создает сам разработчик.

    С точки зрения сервера все вызовы также осуществляются с помощью указателя на интерфейс. Но теперь указатель должен действовать в контексте процесса серверного приложения. Если процессы совпадают (inproc-server), то можно обойтись без заглушек, но если нет, то нужен еще один посредник, который расположен в пространстве серверного процесса.

    Для того чтобы клиент, написанный на любом из перечисленных (элитных) языков, мог вызвать метод интерфейса из СОМ-объекта, расположенного в рамках другого процесса, несколько компонентов должны объединить свои усилия. Прежде всего это две заглушки (клиентская и серверная). В технологии RPC (Remote Procedure Call) они так и называются. В СОМ клиентская заглушка называется proxy stub, или просто proxy (представитель интересов сервера).

    Когда клиент вызывает метод локального или удаленного сервера (Рисунок 8.1), этот вызов перехватывается представителем настоящего сервера, расположенным в адресном пространстве клиента (proxy). Последний получает запрос на вызов метода, упаковывает параметры, которые будут посланы серверу, и вызывает соответствующий метод при помощи RPC. Акт передачи данных, то есть параметров функций и возвращаемых значений, за пределы процесса называется транспортировкой. Она включает в себя упаковку, передачу и распаковку данных по достижении ими места назначения. Отметьте, что транспортировать надо как данные, так и интерфейсные указатели.

    С другой стороны, специальная часть кода на сервере (stub), получает от proxy запрос на вызов метода, распаковывает параметры и вызывает нужный метод реального сервера. Сервер, выполнив клиентский запрос, обычно возвращает какие-то данные. Посредник на стороне сервера (stub) перехватывает эти данные, упаковывает их и направляет соответствующему посреднику на стороне клиента (proxy). Последний получает возвращаемые данные, распаковывает их и передает клиенту. Библиотеки СОМ автоматически обеспечивают код функций proxy/ stub для стандартных интерфейсов. При написании же собственных интерфейсов следует пользоваться интерфейсом, производным от iMarshal. Итак, заместитель расположен в адресном пространстве клиента и представляет интересы СОМ-объекта на стороне клиента, обеспечивая суррогатные точки входа для каждого из методов, обозначенных в исходном IDL-файле. Когда клиент делает вызов удаленной (remote) процедуры сервера, то сначала он вызывает суррогат этой процедуры в заглушке proxy (в пространстве своего процесса). Последняя осуществляет:

  • упаковку параметров в буфер сообщения (message buffer), так чтобы они надежно могли быть доставлены удаленному серверу;
  • вызов библиотечной процедуры передачи параметров в адресное пространство сервера;
  • распаковку выходных (out) или возвращаемых (retval) параметров и передачу их вызывающей процедуре.
  • Серверная заглушка, или просто stub, распаковывает (unmarshals) параметры и передает их объекту СОМ. Она также запаковывает ответную информацию, возвращаемые параметры, для того чтобы передать их назад клиенту.

    Описанные действия называются маршализацией аргументов. Эта процедура сильно зависит от типа параметров. Например, маршализация массива данных значительно сложнее маршализации переменной целого типа или указателя на структуру. Для каждого типа данных существуют свои отдельные функции. Proxy состоит из части, которая размещена в OLE32. DLL (proxy manager), и частей, которые зависят от интерфейсов СОМ-объекта (interface proxies). Для клиента proxy представляет собой реальный СОМ-объект.

    Сам канал передачи обслуживается функциями библиотеки СОМ. Канал передает буфер (с маршализованными параметрами) во владение функциям из RPC-библиотеки, которые и занимаются его передачей через границу между процессами. Вы можете выбирать между стандартной маршализацией, обеспечиваемой библиотекой СОМ, и своей собственной (custom marshaling). В последнем случае вы должны разработать интерфейс, производный от IMarshal. Каждый отдельный интерфейс может пользоваться одним из двух способов маршализации своих параметров. Это определяется на этапе проектирования СОМ-класса, реализующего интерфейсы. Здесь уместно привести схему, которую вы также можете увидеть в MSDN (Search > Marshaling Details).



    Модель программирования COM

    Модель программирования COM

    Любой программный продукт представляет собой набор данных и функций, которые как-то используют, обрабатывают эти данные. Этот принцип, как вы знаете, лежит в основе ООП. Там класс инкапсулирует данные и методы, которые служат для управления ими. Сходный принцип используется и в модели программирования СОМ. СОМ-объектом (или OLE-объектом) называется такой программный продукт, который обеспечивает доступ к данным с помощью одного или нескольких наборов функций, которые называются интерфейсами.
    В отличие от ООП, которое рассматривает интеграцию классов на уровне исходных модулей — текстов программ, СОМ рассматривает интеграцию компонентов на двоичном уровне, то есть на уровне исполняемых модулей. Цель — многократное использование удачно разработанных компонентов именно на этом уровне. Двоичный уровень дает независимость от аппаратной архитектуры и языков программирования (взамен порождая массу других проблем). Двоичный стандарт взаимодействия позволяет СОМ-объектам, разработанным разными поставщиками и на разных языках, эффективно взаимодействовать друг с другом. С практической точки зрения СОМ — это набор системных библиотек (DLL-файлов), которые дают возможность разным приложениям, выполненных с учетом требований СОМ, взаимодействовать друг с другом. Исторически сложилось так, что СОМ состоит из нескольких различных технологий, которые пользуются услугами друг друга для формирования объектно-ориентированной системы. Каждая технология реализует определенный набор функций.

    Преимуществами двоичных компонентов являются: взаимозаменяемость, возможность многократного использования, возможность параллельной разработки с последующей сборкой в одном проекте. Недостатки СОМ настолько очевидны, что я не буду их перечислять. Вы почувствуете их в тот момент, когда начнете самостоятельно разрабатывать свой первый СОМ-объект. Приведем далеко не полный список литературы, который поможет более детально разобраться в технологии СОМ.

  • Kraig Brockschmidt. Inside OLE 2nd Edition, MSDN, Books.
  • Адам Деннинг. ActiveX для профессионалов. — СПб.: Питер, 1998.
  • Д. Бокс. Сущность технологии СОМ. Библиотека программиста. — СПб.: Питер, 2001.
  • С. Холзнер. Visual C++6: учебный курс. — СПб.: Питер, 2001.
  • Д. Круглински, С. Уингоу, Дж. Шеферд. Программирование на Microsoft Visual C++ для профессионалов. — СПб.: Питер, 2001.
  • Д. Эпплман. Win32 API и Visual Basic. Для профессионалов (+CD). — СПб.: Питер, 2001.
  • СОМ реализует модель «клиент-сервер». Объекты, называемые серверами, предоставляют набор функций в распоряжение других объектов, называемых клиентами, но СОМ-объект может быть одновременно и клиентом, и сервером. Серверы всегда подчиняются спецификациям СОМ, в то время как клиенты могут быть как СОМ-объектами, так и не быть таковыми. Поставщик СОМ-объектов (сервер) делает объекты доступными, реализуя один или множество интерфейсов. Пользователь СОМ-объектом (клиент) получает доступ к объекту с помощью указателя на один или множество интерфейсов. С помощью указателя клиент может пользоваться объектом, не зная даже как он реализован и где он находится, но быть при этом уверенным, что объект всегда будет вести себя одинаково. В этом смысле интерфейс объекта представляет собой некий контракт, обещающий клиенту надежное поведение, несмотря на язык и местоположение клиента. Благодаря этому решается проблема бесконечных обновлений версий сервера. Новая версия сервера просто добавляет новые интерфейсы, но никогда не изменяет старых. Клиент может либо пользоваться новым интерфейсом, если он о нем знает, либо не пользоваться им, а вместо этого пользоваться старым. Добавление новых интерфейсов никак не влияет на работу клиентов, работающих со старыми. Кроме того, как нас уверяет документация, двоичный уровень делает компоненты независимыми от платформы клиента.

    Независимость от языка

    Независимость от языка
    Разработанный DLL СОМ-сервер выполняет свою функцию, обслуживая клиентское приложение, разработанное на языке C++. Но он не будет работать с приложениями, написанными на других языках. В MS-документации под другими языками имеют в виду СОМ-совместимые языки: VB, VBScript, Visual J++ и С в версии Microsoft. Остальные платформы и языки пренебрегают технологией СОМ и поэтому как бы не существуют.

    Так вот, чтобы сделать наш объект доступным из клиентского приложения, разработанного на одном из перечисленных четырех языков, надо познакомиться с еще одним внушительным пластом технологии СОМ. Это язык MIDL (Microsoft Interface Definition Language) и компилятор этого языка (MIDL compiler), который тоже иногда называют просто MIDL. Язык MIDL имеет достаточно много новых для C++ ключевых слов, которые более точно описывают атрибуты интерфейсов, классов и их методов, но он не имеет никаких исполняемых операторов (типа for, if и т. д.). Предположим, что вы создали файл MyCom.idl, в котором более точно описали интерфейсы, класс объекта СОМ и библиотеку его типов. В результате компиляции вашего IDL-файла будут сгенерированы несколько других файлов. В их число входят две заглушки MyCom_i.c и МуСот_р.с на языке С и файл заголовков MyCom.h. Эти файлы теперь можно использовать для обеспечения интерфейса между клиентским и серверным приложениями.

    Все начиналось с языка С, но потом было решено, что другие языки тоже должны участвовать в движении СОМ. Проблема совместимости языков возникает потому, что типы данных, используемые в языке С, не совпадают с типами в других языках. Более того, в некоторых из этих языков переменная может по прихоти разработчика изменять свой тип по ходу программы, что совершенно неприемлемо в логике С и C++. В связи с этим и был разработан метаязык более высокого уровня, который используется только для определений (definitions) всех данных, связанных с объектами СОМ, и сопряжения их типов. MIDL пришел на смену языку ODL (Object Description Language) и его компилятору MkTypeLib. Кроме тогЪ, вы можете встретить упоминания о стандарте DCE RFC IDL (Distributed Computing Environment Remote Procedure Call Interface Definition Language), который тоже устарел, так как не поддерживает определений, связанных с объектами.

    При использовании технологий Microsoft вы всегда должны быть готовыми к тому, что для обозначения тех же самых или слегка модифицированных понятий изобретаются абсолютно новые термины, носящие, на мой взгляд, более рекламный, чем смысловой характер. Делая заплату на какие-то явные (или не очень) промахи, целесообразно представить ее в виде новой, даже революционной, технологии, так как этот факт повышает marketability (конкурентоспособность). Но для разработчика это означает лишь дополнительные усилия на выделение истинной сути новшеств и поиск тождественных или сходных понятий, без которых трудно выстроить более или менее стройную модель или структуру, призванную помогать в разработке приложений.

    Новый проект

    Новый проект
    Для ознакомления с возможностями MIDL создайте новый пустой проект типа Win32 DLL. Для этого:

  • Дайте команду File > New > Project. В диалоге New Project выберите шаблон Win32 Project под именем MyComTLib и нажмите ОК. ,
  • В окне Win32 Application Wizard откройте вкладку Application Settings, установите переключатель Application Type в положение DLL, включите флажок Empty Project и нажмите кнопку Finish.
  • Дайте команду Project > Add New Item. В диалоге Add New Item выберите шаблон MIDI File(.idl), задайте имя файла MyComTLib.idl и нажмите кнопку Open.
  • В окне редактора появится новый документ — заготовка описания СОМ-объек-та на языке MIDL Введите в него текст, приведенный ниже:
  • //====== Импорт библиотечных определений

    import "oaidl.idl";
    import "ocidl.idl";

    //====== Уточненное описание интерфейса ISay

    [

    object, uuid(170368DO-85BE-43af-AE71-053F506657A2) ,

    helpstring("My Test DLL COM-server ISay")
    ]

    interface ISay : lUnknown
    {

    HRESULT Say();

    HRESULT SetWord([in]BSTR word);
    }

    //====== Описание библиотеки типов

    [

    uuid(0934DA90-608D-4107-9ECC-C7E828AD0928),

    version(1.0),

    helpstring("My Test DLL COM-server Type Library")
    ]

    library MyCom {

    importlib("stdole32.tlb") ;

    [uuid(9B865820-2FFA-lld5-98B4-OOE0293F01B2)]

    //====== Описание класса реализации интерфейса

    coclass CoSay
    {

    [default] interface ISay; };
    };

    Попробуйте откомпилировать новый файл описания интерфейса, используя клавиатурную комбинацию Ctrl+F7. Если на этом этапе возникнут ошибки, то проверьте настройку проекта View > Property Pages > MIDL > General > MkTy ре Lib Compatible (она должна быть в состоянии No) и повторите компиляцию. После успешного ее завершения просмотрите содержимое папки проекта. В ней должны появиться новые файлы: MyComTLib_h.h, MyComTLibJ.c, MyComTLib_p.c и dlldata.c. Эти файлы, как было сказано, помогают обеспечить взаимодействие клиента с сервером. В результате их компиляции и сборки будет сгенерирована DLL, в которой реализованы коды заглушек proxy/stub.

  • MyComTLib_h.h содержит описания заглушек и интерфейса isay на двух языках: С и C++. Работа с указателями vtable в языке С ведется значительно более изощренным способом, чем в языке C++. В конце файла вы можете увидеть набор макросов, которые сгенерировал MIDL для упрощения этой задачи.
  • MyComTLibJ.c содержит идентификаторы интерфейса, его класса и библиотеки типов. Этот файл должен быть подключен к любому программному модулю, который обращается к нашему интерфейсу ISay.
  • MyComTLib_p.c содержит исходный код заглушек (proxy/stub) для интерфейса. Он, как вы помните, обеспечивает стандартный маршалинг параметров. Код достаточно замысловатый и малопонятный, но его никогда не надо корректировать.
  • dlldata.c содержит несколько макросов. В результате компиляции файла dlldatax в коде DLL заглушек proxy/stub появятся функции DllMain, DllGetclassObject, DllCanUnloadNow, DllRegisterServer И DllUnRegisterServer, которые необходимы всем саморегистрирующимся DLL.
  • Для того чтобы двинуться дальше, вам необходимо взять некоторые файлы из папки МуСот с предыдущим проектом типа DLL.

  • Скопируйте и вставьте в папку текущего проекта файлы MyCom.h, MyCom.cpp, MyCom.reg и MyCom.def, но не переносите файл interfaces.h.
  • Подключите их к проекту. Замените в файле MyCom.cpp директиву #include"interfaces.h" па tinclude "MyComTLib_i . с", а в файл MyCom.h вставьте новую директиву #include "MyComTLibJi.h".
  • Измените содержимое файла MyCom.def так, чтобы оно учитывало создание новой DLL:
  • MyComTLib.def : Declares the module parameters. LIBRARY "MYCOMTLIB.dll"

    EXPORTS .
    DllGetclassObject PRIVATE
    DllCanUnloadNow PRIVATE

    От сырых COM API к проекту ATL

    От сырых COM API к проекту ATL
  • Модель программирования COM
  • Разработка сервера
  • Разработка клиентского приложения
  • Проект на основе ATL
  • Как работает DLL
  • Загадочные макросы
  • Создание элемента типа ATL Control
  • Двойственные интерфейсы
  • В этом уроке мы научимся разрабатывать приложения, которые реализуют функции СОМ-сервера и СОМ-контейнера. Известная вам технология OLE (Object Linking and Embedding) базируется на модели COM (Component Object Model), которая определяет и реализует механизм, позволяющий отдельным компонентам (приложениям, объектам данных, элементам управления, сервисам) взаимодействовать между собой по строго определенному стандарту. Технология разработки таких приложений кажется довольно сложной для тех, кто сталкивается с ней впервые. Трудности могут остаться надолго, если не уделить достаточно времени самым общим вопросам, то есть восприятию концепции СОМ (Модель многокомпонентных объектов). Поэтому не жалейте времени и пройдите через все, даже кажущиеся примитивными, этапы развития СОМ-приложений, как серверов, так и контейнеров. Мы начнем с того, что создадим СОМ-сервер с помощью сырых (raw) COM API-функций для того, чтобы лучше понять механизмы взаимодействия компонентов. Эти механизмы будут частично скрыты в следующих приложениях, которые мы будем развивать на основе стартовых заготовок, созданных мастером Studio.Net в рамках возможностей библиотеки шаблонов ATL (Active Template Library).

    Проект на основе ATL

    Проект на основе ATL
    Библиотеки шаблонов, такие как ATL (Active Template Library), отличаются от обычных библиотек классов C++ тем, что они представляют собой множество шаблонов (templates), которые могут и не иметь иерархической структуры. При использовании обычной библиотеки мы создаем класс, производный от какого-то класса из библиотеки и тем самым наследуем всю его функциональность, а значит, и функциональность его предков. С библиотекой шаблонов поступают по-другому. Выбрав шаблон, обращаются к нему для создания нового, класса, .скроенного по образу и подобию шаблона, получая тем самым его общую функциональность. Специфика определяется путем реализации некоторых методов шаблона. Новый класс кроится по шаблону, настраиваемому параметром, который передается в угловых скобках шаблона.

    Использование библиотеки ATL полностью снимает с вас заботу о реализации методов ILJnknown, о получении уникальных идентификаторов и регистрации их в системе, а также многие другие рутинные проблемы, связанные с поддержкой технологии СОМ. Вы теперь сможете оценить эти преимущества, так как попробовали создать СОМ-объект с помощью сырых (raw) COM API. У нас нет времени более подробно заниматься технологией СОМ, так как общая направленность книги — использование передовых технологий, а не детальное их изучение. Для получения фундаментальных знаний о технологии мы отсылаем читателя к книгам, перечисленным ранее. Отметим, что текст книги Inside OLE целиком (1200 страниц) помещен в MSDN (см. раздел Books).

    Далее рассмотрим, как создать СОМ-объект, обладающий возможностями DLL-сервера (inproc server), Мы создадим новый проект, а в нем остов СОМ DLL-сервера и добавим необходимый нам код, учитывающий специфику СОМ-объекта.
  • На странице VS Home Page выберите гиперссылку Create New Project.
  • В окне диалога New Project выберите тип проекта: Win32 Projects, в окне Templates выберите ATL Project, задайте имя проекта ATLGL и нажмите ОК.
  • В окне мастера ATL Project Wizard выберите вкладку Application Settings и установите переключатель Server Type в положение Dynamic Link Library (сокращенно DLL). Остальные флажки должны быть выключены.
  • Нажмите кнопку Finish.
  • Итак, СОМ DLL-сервер или дом для ко-классов готов. Теперь можно начать процесс начинки его классами (или одним классом), которые, в свою очередь, будут являться домами для экспонируемых интерфейсов. Говорят, что ко-класс реализовывает или экспонирует интерфейсы (или один интерфейс). Просмотрите результаты работы мастера. В файле ATLGL.cpp, здесь уже нарушена традиция MFC разделять объявление и реализацию класса, объявлен класс CATLGLModule, скроенный по шаблону и одновременно производный от класса CAtlDllModuleT. К сожалению, документация по ATL содержит весьма краткие сведения о своих классах. Из нее мы можем, однако, узнать, что шаблон классов CAtlDllModuleT поддерживает функциональность DLL-модуля, который умеет регистрировать себя в качестве такового. Он происходит от класса CAtiModule, у которого есть симметричный потомок CAtlExeModuleT, поддерживающий функциональность ЕХЕ-модуля приложения, и умеет обрабатывать параметры командной строки. Иначе такой модуль называется out-of-proc-сервером (локальным или удаленным сервером). Он выполняется в пространстве собственного процесса, а не клиентского, как в случае in-proc-сервера.

    Аналогично MFC-проекту, в котором есть объект theApp, здесь объявлен глобальный объект _AtlModule класса CATLGLModule, унаследованные методы которого позволяют зарегистрировать (DllRegisterServer) в системном реестре наличие нового сервера COM DLL. Но это только начало. Немного позже мы создадим и зарегистрируем СОМ-объект, все его интерфейсы и библиотеку (typelib) упреждающего описания новых объектов COM (coclass, interface, dispinterface, module, typedef). Да, каждый СОМ-объект вносит довольно много записей в системный реестр, поэтому так важно правильно производить обратную процедуру (DllUnregisterServer), иначе реестр превращается в кладбище записей, внесенных объектами, которые уже не существуют в операционной системе.

    Разработка клиента

    Разработка клиента

    с использованием специальных указателей
    Создайте новый пустой проект консольного приложения с именем SayTLibClient и вставьте в него новый файл SayTLibClient.cpp. Введите в файл следующий текст и проследите за тем, чтобы текст директивы #import либо не разрывался переносом ее продолжения на другую строку, либо разрывался по правилам, то есть с использованием символа переноса ' \ ', как вы видите в тексте книги. После этого запустите проект на выполнение (Ctrl+F5):

    #import "C:\MyProjects\MyComTLib\Debug\ MyComTLib.tlb" \
    no_namespace named_guids

    void main()
    {

    Colnitialize(0);

    //====== Используем "умный" указатель

    ISayPtr pSay(CLSID_CoSay);

    pSay->Say();

    pSay->SetWord(L"The client now uses smart pointers!");

    pSay->Say();

    pSay=0;

    CoUninitialize();
    }

    Несмотря на то что здесь нет многих строчек кода, присутствовавшего в предыдущей версии клиентского приложения, новая версия тоже должна работать. Попробуем разобраться в том, как это происходит.

  • Во-первых, здесь использована директива #import, которая читает информацию из библиотеки типов MyComTLib. tlb и на ее основании генерирует некий код C++. Этот код участвует в процессе компиляции и сборки выполняемого кода клиента. Новый код является неким эквивалентом библиотеки типов и содержит описания интерфейсов, импортированные из TLB-файла.
  • Во-вторых, мы создаем и используем так называемый smart pointer («умный» указатель pSay) на интересующий нас интерфейс. Он берет на себя большую часть работы по обслуживанию интерфейса.
  • Примечание
    Примечание

    Директивой tfimport можно пользоваться для генерации кода не только на основе TLB-файлов, но также и на основе других двоичных файлов, например ЕХЕ-, DLL- или OCX-файлов. Важно, чтобы в этих файлах была информация о типах СОМ-объекте в.
    Вы можете увидеть результат воздействия директивы #import на плоды работы компилятора C++ в папке Debug. Там появились два новых файла заголовков: MyCoTLib.tlh (type library header) и MyComTLib.tli (type library implementations). Первый файл подключает код второго (именно в таком порядке) и они оба компилируются так, как если бы были подключены директивой #include. Этот процесс конвертации двоичной библиотеки типов в исходный код C++ дает возможность решить довольно сложную задачу обнаружения ошибок при пользовании данными о СОМ-объекте. Ошибки, присутствующие в двоичном коде, трудно диагностировать, а ошибки в исходном коде выявляет и указывает компилятор. В данный момент важно не потерять из виду цепь преобразований:

  • какая-то часть исходного текста СОМ-сервера (IDL-файл) была сначала преобразована в двоичный код библиотеки типов (TLB-файл);
  • затем на стороне клиента и на основании этого кода компилятор C++ сгенерировал рассматриваемый сейчас исходный код C++ (TLH- и TLB-файлы);
  • после этого компилятор вновь превращает исходный код в двоичный, сплавляя его с кодом клиентского приложения.
  • Немного позже мы рассмотрим содержимое новых файлов, а сейчас обратите внимание на то, что директива # 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 > ISayPtr;

    Как вы, вероятно, догадались, лексемы _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-указатели помогают обрабатывать ошибки и упрощают поддержку счетчиков ссылок.

    Примечание
    Примечание

    В рассматриваемом коде использован специальный miacc_bstr_t предназначенный для работы с Unicode-строками. Он является классом-оберткой для BSTR, упрощающим работу со строками типа B.STR. Теперь можно не заботиться о вызове функции SysFreeString, так как эту работу берет на себя класс _bstr_t.

    Разработка клиентского приложения

    Разработка клиентского приложения

    Для разработки минимального приложения, способного найти DLL COM inproc-сервер, можно начать с заготовки простого приложения консольного типа, инициализировать системные COM DLL и обратиться к ним с просьбой найти наш СОМ-объект и загрузить DLL в адресное пространство нашего процесса. Все это делается при вызове функции CoGetclassObject из семейства сом API. Обратите внимание на то, что нам не надо изменять настройки проекта (Project > Settings) и указывать компоновщику на необходимость подключения DLL, а также указывать ее локальный или сетевой адрес. Собственно, в этом и есть главная заслуга СОМ. Приложение-клиент можно перенести на другую машину, и если там зарегистрирован наш СОМ-объект, то он будет найден и правильно загружен. Функция CoGetclassObject одновременно с поиском и загрузкой DLL СОМ-серве-ра возвращает адрес запрошенного интерфейса. В нашем случае — это isay. Имея адрес интерфейса, можно обращаться к его методам, управляя, таким образом, объектом.

  • Создайте новый проект типа Win32 с именем SayClient.
  • На странице Application Settings выберите тип Console Application и флаг Empty project.
  • Добавьте в проект новый файл с именем SayClient.cpp.
  • Скопируйте из папки предыдущего проекта и вставьте в папку текущего проекта файл interfaces.h. Подключите его к проекту.
  • Введите в файл SayClient.cpp текст единственной функции main:
  • #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. Этот текст задан клиентским приложением, а воспринят и воспроизведен СОМ-объектом. Если объект не работает, то терпеливо проверьте все этапы создания сервера. В модели СОМ существует довольно много мест, где можно допустить ошибку. Наиболее вероятны ошибки в процессе регистрации.

    Регистрация библиотеки типов

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

    Схема коммуникации клиентсервер

    Рисунок 8.1. Схема коммуникации клиент-сервер

    Схема коммуникации клиентсервер СОМ не накладывает ограничений на структуру компонентов, он определяет лишь порядок их взаимодействия. В основе межпроцессной коммуникации лежит все та же косвенная адресация (таблица виртуальных функций), которая позволяет передать управление либо прямо методу интерфейса (inproc-server), либо его представителю (proxy) на стороне клиента, который, в свою очередь, делает RPC (удаленный вызов) метода настоящего объекта. Прозрачность СОМ-объекта для клиента заключается в том, что proxy-объект знает, где расположен реальный объект (на другом компьютере — remote server, или на том же самом — local server), а клиент об этом не знает.

    Когда клиент хочет использовать СОМ-сервер, он обращается к системной библиотеке с просьбой найти и загрузить сервер, чей CLSID равен определенному значению. Заодно клиент передает IID требуемого интерфейса. В ответ на это системная COM DLL вызывает SCM (Service Control Manager) — менеджер сервисов, который запускается во время загрузки системы. SCM является RFC-сервером, который использует системный реестр, чтобы выполнить поиск реализации, то есть отыскать ЕХЕ- или DLL-файл, содержащий требуемый СОМ-сервер. Чтобы найти модуль сервера, SCM ищет в реестре его CLSID. Если он найден, то SCM возвращает связанный с ним файловый путь, а СОМ загружают этот модуль в память. Теперь возможны два варианта действий: если сервер находится в ЕХЕ-файле, то СОМ запускает его, устанавливает канал связи (RPC) между представителями клиента и сервера (proxy/stub) и возвращает интерфейсный указатель клиенту. Если СОМ-сервер находится в DLL-файле, СОМ просто передаст клиенту указатель на фабрику классов сервера.

    Создание элемента типа ATL Control

    Создание элемента типа ATL Control

    Создаваемый модуль DLL будет содержать в себе элемент управления, который внедряется в окно клиентского приложения, поэтому в проект следует добавить заготовку нового СОМ-класса, обладающего функциональностью элемента типа ATL Control. В следующем уроке мы внесем в него функциональность окна OpenGL, поэтому мы назовем класс OpenGL, хотя в этом уроке элемент не будет иметь дело с библиотекой Silicon Graphics. Он будет элементом ActiveX, созданным на основе заготовки ATL. Создать вручную элемент ActiveX достаточно сложно, поэтому воспользуемся услугами еще одного мастера Studio.Net. При включении нового мастера (wizard) важно, где установлен фокус. Отметьте, что сейчас в рабочем пространстве существуют два проекта: один (ATLGL) — это DLL-сервер, а другой (ATLGLPS) — это коды заглушек proxy/stub.

  • Установите фокус на элемент ATLGL в дереве Solution Explorer и в меню Project выберите команду Add Class (при этом важно, чтобы фокус стоял на имени проекта ATLGL).
  • В окне диалога Add Class выберите категорию ATL, Templates ATL Control и нажмите кнопку Open.
  • В окне мастера ATL Control Wizard выберите вкладку Names и в поле Short Name введите OpenGL.
  • Перейдите на вкладку Attributes и установите следующие значения переключателей и флажков: Control Type: Standard Control, Aggregation: Yes, Threading Model: Apartment, Interface: Dual, Support: Connection Points.
  • Просмотрите и оставьте по умолчанию установки на вкладке Interfaces. Они сообщают о том, что создаваемый класс будет поддерживать шесть интерфейсов: IDataObject, IPersistStorage, IProvideClassInfoZ, IQuickActivate, ISpedfyPropertyPages и ISupportErrorlnfo.
  • На вкладке Miscellaneous поднимите флажок Insertable.
  • На вкладке Stock Properties найдите и добавьте свойство Fill Color, нажав кнопку Add.
  • Нажмите кнопку Finish.
  • Просмотрите результаты работы мастера. Самым крупным его произведением является файл OpenGLh, который содержит объявление и одновременно коды класса COpenGL. Для ATL-проектов характерно то, что создаваемые ко-классы наследуют данные и методы от многих родителей, в число которых входят как СОМ-классы, так и интерфейсы. Другой характерной чертой является сосредоточение значительной части функциональности в h-файле. Напрашивается вывод, что некоторые принципы и идеи, отстаиваемые Microsoft в MFC, были инвертированы в ATL. Сколько полемического задора было растрачено в критике множественного наследования (намек на Borland OWL) на страницах документации по MFC, и вот теперь мы видим вновь созданный класс (COpenGL), который имеет 18 родителей, среди которых 5 классов и 13 интерфейсов.

    Здесь у вас опять должна закружиться голова, но не сдавайтесь. Важно не выпускать главную нить логики приложения. Резон таков: мастера настрочили уйму кода, который пока непонятен, возможно, и всегда будет таким, но этот код уже работает и нам нужно где-то встроиться в него, чтобы почувствовать возможность управлять общей логикой внедряемого элемента ActiveX. Имея под рукой Wizards Studio.Net, это можно сделать, даже оставаясь в некотором неведении относительно деталей работы интерфейсов СОМ. Вам не придется вручную реализовывать ни одного интерфейса. Вы можете сосредоточиться только на алгоритме работы самого элемента, то есть на том, что вы должны продемонстрировать пользователю вашего объекта.

    Запустите приложение, но на этот раз не закрывайте тестовый контейнер, который должен запуститься автоматически, без вашего участия. В окне тестового контейнера вы не увидите признаков нашего элемента, так как он еще не загружен. Дайте команду Edit > IhsertNew Control. После некоторой паузы, в течение которой контейнер собирает информацию из реестра обо всех элементах OLE Controls, вы увидите диалоговое окно с длинным списком элементов, о которых есть информация в реестре.

    Примечание
    Примечание

    Это совсем не означает, что все элементы живы и здоровы. На мой взгляд, ситуация уже вырастает в серьезную проблему. В систему следует ввести эффективные средства корректировки реестра, потому что совсем неинтересно проводить часы драгоценного времени, копаясь в реестре или инструменте типа OLE/COM Object Viewer (Просмотр объектов OLE/COM) и выясняя, жив элемент или его давно нет. Может быть, как говорят политики, я не владею информацией, но все программки типа CleanRegistry либо опасны, либо мало полезны и неэффективны.
    При открытом окне диалога Insert Control вы можете просто ввести букву о — начальную букву нашего элемента OpenGL. Теперь, используя клавиши навигации по списку (стрелки), быстро найдете в нем строку OpenGL Class. Выберите ее и нажмите ОК. Вы должны увидеть окно внедренного элемента, которое выглядит так, как показано на Рисунок 8.2.



    Создание класса СОМобъекта

    Создание класса СОМ-объекта
    Подключите к проекту новый файл MyCom.h, в который надо поместить объявление класса CoSay. Как вы помните, он должен быть потомком экспортируемого интерфейса iSay и дать тела всем методам, унаследованным от всех своих абстрактных предков (isay, lUnknown). Введите в файл следующие коды:

    #if !defined(MY_COSAY_HEADER)
    #define MY_COSAY_HEADER

    #pragma once

    class CoSay : public ISay

    {

    //=====Класс, реализующий интерфейсы ISay, lUnknown

    public:

    CoSay () ;

    virtual -CoSay();

    // lUnknown

    HRESULT _stdcall Querylnterface(REFIID riid, void** ppv);

    ULONG _stdcall AddRefO;

    ULONG _stdcall Release ();

    // ISay

    HRESULT _stdcall Say();

    HRESULT _stdcall SetWord (BSTR word);

    private:

    //====== Счетчик числа пользователей классом

    ULONG m_ref; , //====== Текст, выводимый в окно

    BSTR m word;
    };

    #endif

    Для реализации тел методов класса CoSay подключите к проекту новый файл МуСоm. срр, в который введите коды, приведенные ниже. Обратите внимание на то, как принято работать со строками текста типа BSTR:

    #include "interfaces.h"

    #include "MyCom.h"

    //====== Произвольный ограничитель длины строк

    #define MAX_LENGTH 128

    CoSay::CoSay()
    {

    //=== Обнуляем счетчик числа пользователей класса,

    //=== так как интерфейс пока не используется

    m_ref = 0;

    //=== Динамически создаем строку текста по умолчанию

    m_word = SysAllocString (L"Hi, there."

    "This is MyCom speaking");

    }

    CoSay::-CoSay()
    {

    //=== При завершении работы освобождаем память

    if (m_word)

    SysFreeString(m_word);
    }

    //====== Реализация методов lUnknown

    HRESULT _stdcall CoSay::QueryInterface(REFIID riid, void** ppv)
    {

    //====== Стандартная логика работы с клиентом

    //====== Поддерживаем только два интерфейса

    *ppv = 0;

    if (riid==IID_IUnknown)

    *ppv = static_cast(this) ;
    else if (riid==IID_ISay)

    *ppv = static_cast(this) ;
    else

    return E_NOINTERFACE;

    //====== Есть пользователи нашим объектом

    AddRef();
    return S_OK;
    }

    ULONG _stdcall CoSay:-.AddRef ()
    {

    return ++m_ref;
    }

    ULONG _stdcall CoSay::Release()
    {

    if (--m_ref==0) delete this;

    return m_re f;
    }

    //====== Реализация методов ISay

    HRESULT _stdcall CoSay::Say()
    {

    //=== Преобразование типов (из BSTR в char*), которое

    //=== необходимо для использования MessageBox

    char buff[MAX_LENGTH];

    WideCharToMultiByte(CP_ACP, 0, m_word, -1, buff, MAX_LENGTH, 0, 0);

    MessageBox (0, buff, "Interface ISay:", MB_OK);

    return S_OK;
    }

    HRESULT _stdcall CoSay::SetWord(BSTR word)
    {
    //====== Повторное выделение памяти

    SysReAllocString (&m_word, word);

    freturn S_OK;

    }

    Класс, поддерживающий интерфейс, готов. Теперь следует сделать доступным для пользователей СОМ-объекта весь DLL-сервер, где живет ко-класс CoSay. Минимальным набором функций, которые должна экспортировать COM DLL, является реализация только одной функции DllGetClassObject. Обычно ее сопровождают еще три функции, но в данный момент мы рассматриваем лишь минимальный набор. DLL должна создать СОМ-объект и позволить работать с ним, получив, то есть записав по адресу ppv, адрес зарегистрированного интерфейса. Вы, конечно, заметили, что в предложении дважды использовано слово адрес. Именно поэтому параметр ppv имеет тип void** . Введите эту функцию в конец файла МуСот.срр:

    STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv)
    {

    //=== Если идентификатор класса задан неправильно,
    if (rclsid != CLSID_CoSay)

    // возвращаем код ошибки с указанием причины неудачи
    return CLASS_E_CLASSNOTAVAILABLE;

    //====== Создаем объект ко-класса

    CoSay *pSay = new CoSay;

    //=== Пытаемся получить адрес запрошенного интерфейса
    HRESULT hr = pSay->Query!nterface (riid, ppv) ;

    if (FAILED(hr))

    delete pSay;
    return hr;

    }

    Макроподстановка STDAPI при разворачивании превратится в

    extern "С" HRESULT stdcall

    Примечание
    Примечание

    Работа по опознаванию объектов идет с идентификаторами класса (rclsid) и интерфейса (riid). Это является, как считают апологеты СОМ, одной из самых важных черт, которые вносят небывалый уровень надежности в функционирование СОМ-приложений. Весьма спорное утверждение, так как центром всей вселенной как разработчика, так и пользователя становится Windows-реестр, который открыт всем ветрам — как случайным, так и преднамеренным воздействиям со стороны человека и программы. Однако следует согласиться с тем, что уникальная идентификация снимает проблему случайного, но весьма вероятного совпадения имен интерфейсов, разработанных в разных частях света. То же относится и к именам классов, библиотек типов и т. д.

    Стартовая заготовка элемента ActiveX в окне тестового контейнера

    Рисунок 8.2. Стартовая заготовка элемента ActiveX в окне тестового контейнера

    Стартовая заготовка элемента 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 (заглушка на стороне СОМ-сервера). Эти компоненты общаются между собой и решают проблемы Вавилонской башни, то есть преодолевают сложности обмена данными, возникающими из-за того, что клиент и сервер используют различные типы данных — разговаривают на разных языках. Чтобы увидеть проблему, надо ее создать. Интересно то, что при объяснении необходимости этого чудовищного сооружения:

  • idl-файл;
  • новый класс CProxy_iOpenGLEvents в вашем проекте;
  • новый проект ATLGLPS (proxy-stub) в вашем рабочем пространстве;
  • новый тип структур VARIANT, который надо использовать или просто иметь в виду,
  • приводится соображение о том, что программы на разных языках программирования смогут общаться, то есть обмениваться данными. Как мы уже обсуждали, разработчики имеют в виду четыре языка, два из которых реально используются (Visual C++ и Visual Basic), а два других (VBScript и Visual J++) едва подают признаки жизни. Правда здесь надо учесть бурное развитие нового языка с#, который, очевидно, тоже участвует в движении СОМ.

    Откройте файл ATLGLidl и постарайтесь вникнуть в смысл новых записей, не отвлекаясь на изучение языка IDL, который потребует от вас заметных усилий и временных затрат. Прежде всего отметьте, что в библиотеке типов (library ATLGLLib), сопровождающей наш СОМ-объект, появилось описание СОМ-класса

    coclass OpenGL
    {

    [default] interface IQpenGL;

    [default, source] dispinterface _IOpenGLEvents;
    };

    который предоставляет своим пользователям два интерфейса. Я не привожу здесь предшествующий классу OpenGL блок описаний в квадратных скобках, который носит вспомогательный характер. Элементы ActiveX используют события (events) для того, чтобы уведомить приложение-контейнер об изменениях в состоянии объекта в результате действий пользователя — манипуляции посредством мыши и клавиатуры в окне объекта. Найдите описание одного из объявленных интерфейсов:

    dispinterface _IOpenGLEvents
    {

    properties:

    methods:

    };

    Пока пустые секции properties (свойства): и methods (методы): намекают на то, что мы должны приложить усилия и ввести, с помощью инструментов Studio.Net в разрабатываемый СОМ-объект способность изменять свои свойства и экспортировать методы. Информация о втором интерфейсе расположена вне блока, описывающего библиотеку типов:
    interface IQpenGL : IDispatch
    {

    [propput, bindable, requestedit, id(DISPID_FILLCOLOR)]

    HRESULT FillColor([in]OLE_COLOR clr);

    [propget, bindable, requestedit, id(DISPID_FILLCOLOR)]

    HRESULT FillColor([out, retval]OLE_COLOR* pclr);
    };

    Уникальная идентификация объектов

    Уникальная идентификация объектов
    Данные типа GUID (globally unique identifier) являются 128-битными идентификаторами, состоящими из пяти групп шестнадцатеричных цифр,' которые обычно генерирует специальная программа uuidgen, входящая в инструменты Studio.Net. Например, если вы в командной строке Windows наберете

    uuidgen -n2 -s >guids.txt

    то в файле guids.txt получите два уникальных числа вида:

    {12340001-4980-1920-6788-123456789012}
    {1234*0002-4980-1920-6788-123456789012}

    которые можно использовать в качестве ключа регистрации в Windows-реестре. Рекомендуется обращаться к утилите uuidgen и просить сразу много идентификаторов, а затем постепенно использовать их (помечая, чтобы не забыть) в своем приложении для идентификации интерфейсов, СОМ-классов и библиотек типов. Это упрощает отладку, поиск в реестре и, возможно, его чистку. Кроме этого способа существуют и другие. Например, можно обратиться к функции

    HRESULT CoCreateGuid(GUID *pguid);

    которая гарантированно выдаст уникальное 128-битное число, которое не совпадет ни с одним другим числом, полученным в любой вычислительной системе, в любой точке планеты, в любое время в прошлом и будущем. Впечатляюще, не правда ли? Есть целая серия функций вида Uuid* из блока RFC-API, которые генерируют и обрабатывают числа типа GUID. Число, как вы видите, разбито на пять групп, как-то связанных с процессом генерации, в котором задействованы время генерации, географическое место, информация о системе и т. д. Следующие типы переменных эквивалентны типу GUID:

  • CLSID — используются для идентификации СОМ-классов;
  • IID — используются для идентификации СОМ-интерфейсов;
  • UUID (Universally Unique Identifiers) — используются в RPC (Remote Procedure Calls) библиотеках для идентификации клиентов и серверов, а также интерфейсов.
  • Тип IID используется также и для идентификации библиотек типов. Переменные типа GUID являются структурами, содержащими четыре поля. Тип GUID определен в guiddef.h следующим образом:

    typedef struct

    {

    //=== 1-я группа цифр (8 цифр - 4 байта)

    unsigned long Datal;

    //=== 2-я группа цифр (4 цифры - 2 байта)

    unsigned short Data2;

    //=== 3-я группа цифр (4 цифры - 2 байта)

    unsigned short Data3;

    //=== 4-я и 5-я группы (4 и 12 цифр) - 8 байт

    byte Data4[8];
    }
    GUID;

    Мы уже обсуждали необходимость уникальной идентификации интерфейсов. Ну а зачем уникально идентифицировать классы? Предположим, что два разработчика создали два разных СОМ-класса, но оба назвали их MySuperGrid. Так как СОМ узнает класс по его CLSID, а алгоритм генерации CLSID гарантирует его уникальность, то совпадение имен не мешает использовать оба класса в одном клиентском приложении. Система пользуется двумя типами GUID: строковым (применяется в реестре) и числовым (нужен клиентским приложениям).

    Я думаю, что в этот момент у неискушенного СОМ-технологией читателя должна слегка закружиться голова. Это нормально, так как по заявлению авторитетов (David Cruglinsky), она будет кружиться в течение примерно полугода, при условии регулярного изучения СОМ-технологий.

    Загадочные макросы

    Загадочные макросы

    Вернемся в файл ATLGLcpp, где кроме функций, перечисленных выше, присутствуют загадочные макросы. Их смысл довольно прозрачен, но разработчика не должны устраивать догадки, ему нужны более точные знания. Сопровождающая документация, особенно бета-версий, не всегда дает нужные объяснения, поэтому приходится искать их самостоятельно в заголовочных файлах, расположенных по адресу: ...\Microsoft Visual Studio.Net\Vc7\indude или ...\Microsoft Visual Studio.Net\ Vc7\atlmfc\include.
    Покажем, как это делается на примере. Нас интересует смысл функциеподобной макроподстановки:

    DECLARE_LIBID(LIBID_ATLGLLib)

    В результате поиска в файлах по указанному пути (маска поиска должна быть *.h) находим (в файле ATLBase.h), что при разворачивании препроцессором этот макрос превратится в статическую функцию класса CATLGLModule:

    static void InitLibldO throw ()
    {

    CAtlModule::m_libid = LIBID_ATLGLLib;
    }

    Теперь возникает желание узнать, что кроется за идентификатором LiBiD_ATLGLLib. Во вновь созданном коде файла ATLGM.c находим макрос:

    MIDL_DEFINE_GUID(IID,

    LIBID_ATLGLLib,ОхЕбОбОЗВС,Ox9DE2, 0x4563,
    OxA7,0xAF,Ox8A,Ox8C,Ox4E,0x80,0x40,0x58);

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

    #define MIDL_DEFINE_GUID(type,name,1,wl,w2,bl,b2,b3,Ь4, \ Ь5,Ьб,b7,b8)
    const type name = \ {I,wl,w2, {b1,b2,bЗ,b4,b5,b6,b7,b8}
    }

    Подставив значения параметров из предыдущего макроса, получим определение LiBiD_ATLGLLib, которое увидит компилятор:

    const IID LIBID_ATLGLLib =
    {

    0xE60605BC, 0x9DE2, 0x4563,

    { 0xA7,0xAF,0x8A, 0x8C,Ox4E, 0x80, 0x40, 0x58 }
    }

    Отсюда ясно, что LIВID_АТLGLLib — это константная структура типа IID. Осталось узнать, как определен тип данных II D.
    В хорошо знакомом файле afxwin.h находим определение typedef GUID IID;. Про Globally Unique Identifier (GUID) сказано очень много, в том числе и в документации Studio.Net. Как мы только что выяснили, изучив работу макросов и LiBio_ATLGLLib, тип IID также используется для идентификации библиотек типов. Система применяет два типа GUID: строковый в реестре, и числовой в клиентских приложениях. Второй макрос, который вы видели в классе
    CATLGLModule:

    DECLARE_REGISTRY_APPID_RESOURCEID(IDR_ATLGL,

    "{E4541023-7425-4AA7-998C-D016DF796716}")

    (цифры мои, ваши будут другими) создает строковый GUID. При расширении он превратится в три статические функции класса, две из которых готовят текстовую строку того или иного типа, а третья регистрирует, в случае если bRegister==TRUE, или убирает из реестра эту строку по адресу HKEY_CLASSES_ROOT\APPID\:

    static LPCOLESTR GetAppId ()throw ()
    {

    //====== Преобразование к формату OLE-строки

    return OLESTR("{E4541023-7425-4AA7-998C-D016DF796716}") ;

    }

    static TCHAR* GetAppIdTO throw ()

    {

    //====== Преобразование к Unicode или char* строке

    return _T("{E4541023-7425-4AA7-998C-D016DF796716}") ;

    }

    // Если bRegister==TRUE, то происходит запись в реестр,

    // иначе - удаление записи

    static HRESULT WINAPI UpdateRegistryAppId(BOOL bRegister) throw()

    {

    _ATL_REGMAP_ENTRY aMapEntries [] =

    {

    { OLESTRC'APPID") , GetAppIdO }, { NULL, NULL }

    };

    return ATL::_pAtlModule->UpdateRegistryFromResource( IDR ATLGL, bRegister, aMapEntries);

    В данный момент вы сможете найти в реестре свой ключ и ассоциированную с ним строку (ATLGL) по адресу:

    HKEY_CLASSES_ROOT\AppID\
    {E4541023-7425-4AA7-998C-D016DF796716}

    При запуске приложения вышеописанные функции были вызваны каркасом приложения и произвели записи в реестр. Отметьте также, что в реестре появилась еще одна (симметричная) запись по адресу HKEY_CLASSES_ROOT \APPID\ATLGL.DLL. Она ассоциирует строковый GUID с библиотекой ATLGL.DLL. Рассматриваемая строка-идентификатор встречается еще в нескольких разделах проекта, найдите их, чтобы получить ориентировку: в ресурсе "REGISTRY" > IDR_ATLGL (см. окно Resource View) и в файле сценария регистрации ATL.GL.rgs (см. окно Solution Explorer).

    Возвращаясь к первому макросу DECLARE_LIBID(LiBiojvTLGLLib), отметим, что скрытая за ним функция initLibid тоже была вызвана каркасом и использована для регистрации библиотеки типов будущего СОМ-объекта. Вы можете найти эту, значительно более подробную, запись по ключу (цифры мои):

    HKEY_CLASSES_ROOT\TypeLib\
    {E60605BC-9DE2-4563-A7AF-8A8C4E804058}

    Cамоучитель по Visual Studio.Net

    Файловые операции

    Файловые операции

    Создание тестовой поверхности, чтение данных из файла и хранение этих данных в контейнере мы будем делать так же, как и в проекте MFC. Для разнообразия используем другую формулу для описания поверхности по умолчанию, то есть того графика, который увидит пользователь элемента ActiveX при его инициализации в рамках окна контейнера. Вот эта формула:

    Yi,j=exp[-(i+20*j)/256]*SIN[3*п*

    (i-Nz/2)/Nz]*SIN[3*п*(j-Nx/2)/Nx]

    Приведем тело функции Def aultGraphic, которая генерирует значения этой функции над дискретной сеткой узлов в плоскости X-Z и записывает их в файл с именем «expidat». В теле этой функции мы вызываем другую вспомогательную функцию SetGraphPoints, которая наполняет контейнер точек типа CPointSD. При этом, как вы помните, она генерирует недостающие две координаты (z, x) и масштабирует ординаты (у) так, чтобы соблюсти разумные пропорции изображения графика на экране:

    void COGView::DefaultGraphic()
    {

    //====== Размеры сетки узлов

    m_xSize = m_zSize = 33;

    //====== число ячеек на единицу меньше числа узлов

    UINTnz = m_zSize - 1, nx = m_xSize - 1;

    // Размер файла в байтах для хранения значений функции

    DWORD nSize = m_xSize * m_zSize * sizeof(float) + 2*sizeof (UINT);

    //====== Временный буфер для хранения данных

    BYTE *buff = new BYTE[nSize+1];

    //====== Показываем на него указателем целого типа

    UINT *p = (UINT*)buff;

    // Размещаем данные целого типа

    *р++ = m_xSize;

    *р++ = m_zSize;

    //===== Меняем тип указателя, так как дальше

    //====== собираемся записывать вещественные числа

    float *pf = (float*)p;

    // Предварительно вычисляем коэффициенты уравнения

    double fi = atan(l.)*12, kx=fi/nx, kz=fi/nz;

    //=== В двойном цикле пробега по сетке узлов

    //=== вычисляем и помещаем в буфер данные типа float

    for (UINT i=0; i
    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.

    в рамках окна контейнера, будет

    Введение обработчиков сообщений Windows

    Наш объект, будучи активизирован в рамках окна контейнера, будет реагировать на сообщения Windows. Он должен управляться мышью, поддерживать вращение с помощью таймера, устанавливать нужный формат при создании своего окна и т. д. Введите в класс copenGL способность реагировать на следующие сообщения:
    WM_ERASEBKGND, WM_LBUTTONDOWN, WM_RBUTTONDOWN, WM_LBUTTONUP, WM_RBUTTONUP, WM_MOUSEMOVE, WM_CREATE, WM_DESTROY, WM_SIZE, WM_TIMER.
    Для этого:

  • Поставьте курсор на строку с именем класса COpenGL в окне ClassView и дайте команду Properties из контекстного меню.
  • Нажмите кнопку Messages на панели инструментов окна Properties.
  • Для того чтобы после введения обработчика окно свойств не убегало, переведите его в режим Floating и оттащите в сторону. В окне Class View должен быть выбран класс COpenGL
  • По очереди для всех перечисленных сообщений укажите действие в правом столбце таблицы Properties.
  • Обработчик сообщения OnEraseBkgnd вызывается операционной системой в те моменты, когда фон окна должен быть стерт, например при изменении размеров окна. Родительская версия этой функции или обработка по умолчанию использует для стирания (закрашивания) кисть, указанную в структуре WNDCLASS при ее регистрации. Если надо отменить стирание фона, то наша версия функции обработки должна установить специальный флаг, который говорит о том, что сообщение обработано, иначе окно останется помеченным как нуждающееся в стирании фона. Введите в файл реализации класса COpenGL код обработки сообщения:

    LRESULT COpenGL::OnEraseBkgnd(UINT /*uMsg*/, WPARAM

    /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)
    {

    //====== Устанавливаем флаг завершения обработки

    bHandled = TRUE;

    return 0;
    }

    Отметьте, что прототип функции обработки отличается от того, который принят в MFC. Там он имеет вид af x_msg BOOL OnEraseBkgnd(CDC* pDC); и определен в классе CWnd. Наш класс COpenGL среди своих многочисленных предков имеет класс CComControl, который происходит от класса CWindowlmpl, а тот, в свою очередь, является потомком класса cwindow. Последний выполняет в ATL ту же роль, что и класс cwnd в MFC, но не несет с собой бремени наследования от CObject. Это в основном и ускоряет функционирование ATL-приложений.

    Примечание
    Примечание

    В заготовке тела функций обработки все параметры закомментированы. Это сделано для того, чтобы упростить работу компилятору, так как далеко не все параметры задействованы постоянно. Если параметр необходимее его нужно сделать видимым для компилятора, убрав знаки комментария. Сделайте это для параметра bHandled.
    Теперь введите в класс обработчик сообщения WM_CREATE и заполните его кодами, которые готовят окно и устанавливают некоторые параметры OpenGL:

    LRESULT COpenGL::OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/,'LPARAM /*lParam*/, BOOL& bHandled)

    //======= Описатель формата окна OpenGL

    PIXELFORMATDESCRIPTOR pfd =

    {

    sizeof(PIXELFORMATDESCRIPTOR),
    // Размер структуры
    1,
    // Номер версии
    PFD_DRAW_TO_WINDOW |
    // Поддержка
    GDI PFD_SUPPORT_OPENGL |
    // Поддержка OpenGL
    PFD_DOUBLEBUFFER,
    // Двойная буферизация
    PFD_TYPE_RGBA,
    // Формат RGBA, не палитра
    24,
    // Количество плоскостей

    // в каждом буфере цвета

    24, 0,
    // Для компонента Red
    24, 0,
    // Для компонента Green
    24, 0,
    // Для компонента Blue
    24, 0,
    // Для компонента Alpha
    0,
    // Количество плоскостей

    // буфера Accumulation

    0,
    // То же для компонента Red
    0,
    // для компонента Green
    0,
    // для компонента Blue
    0, // для компонента Alpha
    32, // Глубина Z-буфера
    0, // Глубина буфера Stencil
    0, // Глубина буфера Auxiliary
    0, // Теперь игнорируется
    0, // Количество плоскостей
    0, // Теперь игнорируется
    0, // Цвет прозрачной маски
    0 // Теперь игнорируется

    };

    // Добываем дежурный контекст и просим выбрать ближайший

    m_hdc = GetDCO ;

    int iD = ChoosePixelFormat(m_hdc, &pfd) ;

    if ( !ID )
    {

    ATLASSERT(FALSE);

    return -1;
    }

    //====== Пытаемся установить этот формат

    if ( ISetPixelFormat (m_hdc, iD, &pfd))
    {

    ATLASSERT(FALSE);

    return -1;
    }

    //====== Пытаемся создать контекст передачи OpenGL

    if ( !(m_hRC = wglCreateContext (m_hdc)))
    {

    ATLASSERT(FALSE);

    return -1;
    }

    //====== Пытаемся выбрать его в качестве текущего

    if ( !wglMakeCurrent (m_hdc, m_hRC))
    {

    ATLASSERT(FALSE);

    return -1;
    }

    //====== Теперь можно посылать команды OpenGL

    glEnable (GL_LIGHTING) ;
    // Будет освещение
    glEnable (GL_LIGHTO) ;
    // Только 1 источник
    glEnable (GL_DEPTH_TEST) ;
    // Учитывать глубину (ось Z)

    //====== Учитывать цвет материала поверхности

    glEnable (GL_COLOR_MATERIAL) ;
    //====== Устанавливаем цвет фона

    SetBkColor () ;
    bHandled = TRUE;
    return 0;
    }

    Класс copenGL должен реагировать на сообщение WM_SIZE и корректировать видимый объем сцены. Мы будем использовать режим просмотра с учетом перспективы. Его определяет функция gluPerspective. Введите в класс copenGL обработку WM_SIZE и вставьте в нее следующие коды:

    LRESULT COpenGL: :OnSize(UINT /*uMsg*/, WPARAM /*wParam*/,

    LPARAM IParam, BOOL& bHandled)
    {

    // Распаковываем длинный параметр и узнаем размеры окна

    UINT сх = LOWORD ( IParam) , су = HIWORD (IParam) ;

    //====== Вычисляем максимальные диспропорции окна

    double dAspect = cx<=cy ? double (су) /сх

    : double (сх) /су;

    //==== Задаем тип текущей матрицы (матрица проекции)

    glMatrixMode (GL_PROJECTION) ;

    //====== Приравниваем ее к единичной диагональной

    glLoadldentity () ;

    //== Параметры перспективы (45 градусов - угол обзора)
    gluPerspective (45., dAspect, 1., 10000.);
    glViewport (0, 0, сх, су); DrawScene () ;
    bHandled = TRUE;
    return 0;
    }

    Функция glViewport, как вы помните, задает прямоугольник просмотра. При закрытии окна внедренного объекта необходимо освободить память, занимаемую контекстом передачи, и отказаться от услуг таймера, с помощью которого мы будем производить анимацию вращения изображения. Введите в класс обработчик сообщения WM_DESTROY и измените ее стартовый код, как показано ниже:

    LRESULT COpenGL: :OnDestroy (UINT /*uMsg*/, WPARAM

    /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)

    {
    KillTimer(l);
    if (m_hRC)

    {

    wglDeleteContext(m_hRC); m_hRC = 0;
    }

    bHandled = TRUE;
    return 0;
    }

    Инициализация переменных

    В конструктор класса вставьте код установки начальных значений переменных, с помощью которых пользователь сможет управлять сценой Open GL:

    COpenGL: : COpenGL()
    {

    //====== Контекст передачи пока отсутствует

    m_hRC = 0;

    //====== Начальный разворот изображения

    m_AngleX = 35. f;
    m_AngleY = 20. f;

    //====== Угол зрения для матрицы проекции

    m_AngleView = 45. f;

    //====== Начальный цвет фона

    m_clrFillColor = RGB (255,245,255);

    //====== Начальный режим заполнения

    //====== внутренних точек полигона

    m_FillMode = GL_FILL;

    //====== Подготовка графика по умолчанию

    DefaultGraphic ();

    //=== Начальное смещение относительно центра сцены
    //=== Сдвиг назад на полуторный размер объекта
    m_zTrans = -1.5f*m_fRangeX;
    m_xTrans = m_yTrans = 0.f ;

    // Начальные значения квантов смещения (для анимации)

    m_dx = m_dy = 0.f;

    //=== Мыть не захвачена

    m_bCaptured = false;

    //=== Правая кнопка не была нажата

    m_bRightButton = false;

    //=== Рисуем четырехугольниками m_bQuad = true;

    //====== Начальный значения параметров освещения

    m_LightParam[OJ = 50; // X position
    m_LightParam[l] = 80; // Y position
    m_LightParam[2] = 100; // Z position

    m_LightParam[3] = 15; // Ambient light

    m_LightPararn[4] = 70; // Diffuse light

    m_LightParam[5] = 100; // Specular light

    m_LightParam[6] = 100; // Ambient material

    m_LightParam[7] = 100; // Diffuse material

    m_LightParam[8] = 40; // Specular material

    m_LightParam[9] = 70; // Shininess material

    m_LightParam[10] = 0; // Emission material
    }

    Функция перерисовки

    Перерисовка изображения OpenGL состоит в том, что обнуляется буфер цвета и буфер глубины — буфер третьей координаты. Затем в матрицу моделирования (GL_MODELVIEW), которая уже выбрана в качестве текущей, загружается единичная матрица (glLoadldentity). После этого происходит установка освещения, с тем чтобы на него не действовали преобразования сдвига и вращения. Лишь после этого матрица моделирования домножается на матрицу трансляции и матрицу вращений. Чтобы рассмотреть изображение, достаточно иметь возможность вращать его вокруг двух осей (X и Y). Поэтому мы домножаем матрицу моделирования на две матрицы вращений (glRotatef). Сначала вращаем вокруг оси X, затем вокруг оси Y:

    HRESULT COpenGL: :OnDraw (ATL_DRAWINFO& di)
    {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glLoadldentity{);

    //====== Установка параметров освещения

    SetLight ();

    //====== Формирование матрицы моделирования

    glTranslatef(m_xTrans,m_yTrans,m_zTrans);
    glRotatef (m_AngleX, l.0f, 0.0f, 0.0f );
    glRotatef (m_AngleY, 0.0f, l.0f, 0.0f );

    //====== Вызов рисующих команд из списка

    glCallList(1);

    //====== Переключение буферов

    SwapBuffers(m_hdc);
    return S_OK;

    }

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

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

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

  • Вставляют уже зарегистрированный элемент ActiveX в форму приложения контейнера, используя так называемую галерею объектов (Gallery).
  • В одном из классов контейнера определяют переменную того же типа, что и класс-оболочка для внедренного элемента.
  • Программируют поведение элемента, управляя им с помощью этой переменной.
  • Первый шаг этого алгоритма вы уже выполнили, теперь введите в состав проекта два новых файла OpenGLh и OpenGLcpp, которые будут содержать коды класса-оболочки copenGL. Вот содержимое файла заголовков (OpenGLh):

    #pragma once

    //=========== COpenGL wrapper class

    class COpenGL : public CWnd

    {

    protected:

    DECLARE_DYNCREATE(COpenGL)

    public:

    //==== Метод для добывания CLSID нашего элемента

    CLSID const& GetClsidO

    {

    static CLSID const clsid =

    {

    0x519d9ed8, Oxbc4'6, 0x4367,
    { Ox9c, OxcO, 0x49, 0x81, 0x40, Oxf3, 0x94, 0x16 }

    };

    return clsid;
    }

    virtual BOOL Create(LPCTSTR IpszClassName,

    LPCTSTR IpszWindowName, DWORD dwStyle,
    const RECT& rect, CWnd* pParentWnd, UINT nID, CCreateContext* pContext = NULL)
    {

    return CreateControl(GetClsid(), IpszWindowName,

    dwStyle, rect, pParentWnd, nID)
    }

    BOOL Create (LPCTSTR IpszWindowName, DWORD dwStyle,

    const RECT& rect, CWnd* pParentWnd, UINT nID, CFile* pPersist = NULL,
    BOOL bStorage = FALSE, BSTR bstrLicKey = NULL)
    {

    return CreateControl(GetClsidO, IpszWindowName, dwStyle, rect, pParentWnd, nID, pPersist, bStorage, bstrLicKey);
    }

    //====== Методы, экспонируемые элементом ActiveX

    public:

    void SetFillColor(unsigned long newValue);

    unsigned long GetFillColor();

    void GetLightParams(long* pPos);

    void SetLightParam(short Ip, long nPos);

    void ReadData();

    void SetFillMode(DWORD mode);

    void GetFillMode(DWORD* pMode);

    void GetQuad(BOOL* bQuad);

    void SetQuad(BOOL bQuad);
    };

    Самым важным моментом в процедуре вставки класса является правильное задание CLSID того класса OpenGL, который был зарегистрирован в операционной системе при создании DLL-сервера, то есть нашего элемента ActiveX. He пытайтесь сравнивать те цифры, которые приведены в книге, с теми, которые были приведены в ней же до этого момента, так как в процессе отладки пришлось не раз менять как классы, так и целиком приложения. Мне не хочется отслеживать эти жуткие номера. Если вы хотите вставить правильные цифры, то должны взять их из вашей версии предыдущего приложения ATLGL. Например, откройте файл ATLGL.IDL и возьмите оттуда CLSID для ко-класса OpenGL, то есть найдите такой фрагмент этого файла:

    [

    uuid(519D9ED8-BC46-4367-9CCO-498140F39416),

    helpstring("OpenGL Class") ]
    coclass OpenGL

    {

    [default] interface IOpenGL;

    [default, source] dispinterface _IOpenGLEvents;
    };

    И скопируйте первую строку
    uuid(519D9ED8-BC46-4367-9CCO-498140F39416),

    но с вашими цифрами и вставьте ее в качестве комментария в файл OpenGLh нового проекта TestGL. Затем аккуратно, соблюдая формат, принятый для структуры CLSID, перенесите только цифры в поля статической структуры clsid, которую вы видите в методе GetClsid класса-оболочки. Цифры должны быть взяты из принесенной строки, но их надо отформатировать (разбить) по-другому принципу. Например, для нашего случая правильным будет такое тело метода GetClsid:

    CLSID const& GetClsid()
    {

    // Следующая строка взята из файла ATLGL.IDL

    // 519D9ED8-BC46-4367-9CCO-498140F39416

    static CLSID const clsid =
    {

    //======== Эти цифры взяты из файла ATLGL.IDL

    0x519d9ed8, 0xbc46, 0x4367,
    { 0х9с, 0xc0, 0x49, 0x81, 0x40, 0xf3, 0x94, 0x16 } ) ;

    return clsid;
    }

    Кроме этого важного фрагмента в новом классе объявлены два совмещенных метода Create, каждый из которых умеет создавать окно внедренного элемента ActiveX с учетом особенностей стиля окна (см. справку по CWnd: :CreateControl). Затем в классе-оболочке должны быть представлены суррогаты всех методов, экспонируемых классом OpenGL COM DLL-сервера ATLGL.DLL. В том, что вы не полностью приводите тела методов сервера, иначе это был бы абсурд, хотя и так близко к этому, можно убедиться, просмотрев на редкость унылые коды реализации класса-оболочки, которые необходимо вставить в файл OpenGLcpp. Утешает мысль, что в исправной Studio.Net эти коды не придется создавать и редактировать вручную:

    #include "stdafx.h"

    #include "opengl.h"

    IMPLEMENT_DYNCREATE(COpenGL, CWnd)

    //====== Стандартное свойство реализовано

    //====== в виде пары методов Get/Set

    void COpenGL::SetFillColor(unsigned long newValue)
    {
    static BYTE parms[] =

    VTS_I4; InvokeHelper(0xfffffe02, DISPATCH_PROPERTYPUT,VT_EMPTY,

    NULL, parms, newValue);
    }

    //====== Стандартное свойство

    unsigned long COpenGL::GetFillColor0 {

    unsigned long result;

    InvokeHelper (Oxfffffe02, DISPATCH_PROPERTYGET, VT_I4, (void4)&result, NULL);

    return result;
    }
    //====== Наши методы сервера

    void COpenGL::GetLightParams(long* pPos)
    {

    static BYTE parms[] = VTS_PI4;

    InvokeHelper (Oxl, DISPATCH_METHOD, VT_EMPTY, NULL,

    parms, pPos);
    }

    void COpenGL: : SetLightParam (short lp, long nPos)
    {

    static BYTE parms [ ] = VTS 12 VTS 14;

    InvokeHelper{0x2, DISPATCH_METHOD, VT_EMPTY, NULL,

    parms, lp, nPos);
    }

    void COpenGL::ReadData()
    InvokeHelper(0x3, DISPATCH_METHOD, VT_EMPTY, 0, 0) ;

    void COpenGL::GetFillMode(DWORD* pMode)

    static BYTE jparms[] =

    VTS_PI4; InvokeHelper (0x4, DISPATCH_METHOD, VT_EMPTY, NULL,

    parms, pMode);
    }

    void COpenGL::SetFillMode(DWORD nMode)

    static BYTE parms[] =

    VTS_I4;

    InvokeHelper(0x5, DISPATCH_METHOD, VT_EMPTY, NULL, parms, nMode);

    void COpenGL::GetQuad(BOOL* bQuad)

    static BYTE parms[] =

    VTS_PI4;

    InvokeHelper(0x6, DISPATCH_METHOD, VT_EMPTY, NULL, parms, bQuad);

    void COpenGL::SetQuad(BOOL bQuad)

    static BYTE parms[] =

    VTS_I4;

    InvokeHelper (0x7, DISPATCH_METHOD, VT_EMPTY, NULL, parms, bQuad);
    }

    Затем подключите оба новых файла к проекту Project > Add Existing Item.

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

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

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



    Окно ActiveX элемента внедренного в окно тестового контейнера

    Рисунок 9.1. Окно ActiveX элемента, внедренного в окно тестового контейнера

    Окно ActiveX элемента внедренного в окно тестового контейнера STDMETHODIMP COpenGL::GetFillMode(DWORD* pMode)
    {
    //======= Режим заполнения полигонов

    *pMode = m_FillMode;
    return S_OK;
    }

    STDMETHODIMP COpenGL::SetFillMode(DWORD nMode)
    m_FillMode = nMode;

    //====== Построение нового списка команд OpenGL

    DrawScene();

    // Требование получить разрешение перерисовать окно FireViewChange();

    return S_OK;

    STDMETHODIMP COpenGL::GetQuad(BOOL* bQuad)
    //======= Режим построения полигонов

    *bQuad = m_bQuad;
    return S_OK;
    }

    STDMETHODIMP COpenGL::SetQuad(BOOL bQuad)
    {
    m_bQuad = bQuad == TRUE;

    //======= Построение нового списка команд OpenGL

    DrawScene ();

    //======= Просьба о перерисовке

    FireViewChange();
    return S_OK;
    }

    Подготовка сцены OpenGL

    Подготовка сцены OpenGL
    Считая, что данные о координатах точек изображаемой поверхности уже известны и расположены в контейнере m_cPoints, напишем коды функции DrawScene, которая создает изображение поверхности и запоминает его в виде списка команд OpenGL. Как вы помните, одним из технологических приемов OpenGL, которые ускоряют процесс передачи (rendering), является предварительная заготовка изображения, то есть запоминание и компиляция списка рисующих команд.

    Напомним, что отображаемый график представляет собой криволинейную поверхность (например, равного уровня температуры). Ось Y, по которой откладываются интересующие пользователя значения функции, направлена вверх. Ось X направлена вправо, а ось Z — вглубь экрана. Часть плоскости (X, Z), для точек которой известны значения Y, представляет собой координатную сетку. Изображаемая поверхность расположена над плоскостью (X, Z), а точнее, над этой сеткой. Поверхность можно представить себе в виде одеяла, сшитого из множества лоскутов. Каждый лоскут мы будем задавать в виде четырехугольника, как-то ориентированного в пространстве. Все множество четырехугольников поверхности также образует сетку. Для задания последовательности четырехугольников в OpenGL существует пара команд:

    glBegin (GL_QUADS) ;

    // Здесь располагаются команды, задающие четырехугольники

    glEnd() ;

    Четырехугольник задается координатами своих вершин. При задании координат какой-либо вершины, например, командой givertex3f (х, у, z);, можно сразу же определить ее цвет, например, командой gicolor3f (red, green, blue);. Если цвета вершин будут разными, а режим заполнения равен константе GL_FILL, то цвета внутренних точек четырехугольника примут промежуточное значение. Конвейер OpenGL производит аппроксимацию цвета так, что при перемещении от одной вершины к другой он изменяется плавно.

    Режим растеризации или заполнения промежуточных точек графического примитива задается командой glPolygonMode. OpenGL различает фронтальные (front-facing polygons), обратные (back-facing polygons) и двухсторонние многоугольники. Режим заполнения их отличается, поэтому первый параметр функции glPolygonMode должен определить тип полигона (GL_FRONT, GL_BACK или GL_FRONT_AND_BACK).

    Второй параметр собственно и определяет режим заполнения. Он может принимать значение GL_POINT, GL_LINE или GL_FILL. Первый выбор даст лишь обозначение примитива в виде его вершин, второй — даст некий скелет, вершины будут соединены линиями, а третий заполнит все промежуточные точки примитива. По умолчанию принят режим GL_FILL и мы получаем сплошной лоскут.'Если в качестве первого параметра задать GL_FRONT_AND_BACK, то изменения второго параметра будут касаться обеих поверхностей одеяла. Другие сочетания дают на первый взгляд странные эффекты: так, если задать сочетание (GL_FRONT, GL_LINE), то лицевая сторона одеяла будет обозначена каркасом (frame view), а изнаночная по умолчанию будет сплошной (GL_FILL). Поверхность при этом будет полупрозрачна.

    Мы решили оставить неизменным значение GL_FRONT_AND_BACK для первого параметра и дать пользователю возможность изменять режим заполнения (второй параметр glPolygonMode) по его желанию. Впоследствии внесем эту настройку в диалог свойств СОМ-объекта, а результат выбора пользователя будем хранить в переменной m_FillMode. С учетом сказанного введите коды реализации функции DrawScenel

    //====== Подготовка изображения

    void COpenGL::DrawScene()
    {

    //====== Создание списка рисующих команд

    glNewListd, GL_COMPILE) ;

    //====== Установка режима заполнения

    //====== внутренних точек полигонов

    glPolygonMode(GL_FRONT_AND_BACK, m_FillMode);

    //====== Размеры изображаемого объекта

    UINTnx = m_xSize-l, nz = m_zSize-l;

    //====== Выбор способа создания полигонов

    if (m_bQuad)

    glBegin (GL QUADS);

    //=== Цикл прохода по слоям изображения (ось Z) for (UINT z=0, i=0; z
    //=== Связанные полигоны начинаются
    //=== на каждой полосе вновь if (!m_bQuad)

    glBegin(GL_QUAD_STRIP) ;

    //=== Цикл прохода вдоль оси X
    for (UINT x=0; x; х++, i++)
    {

    // i, j, k, n — 4 индекса вершин примитива при
    // обходе в направлении против часовой стрелки
    int j = i + m_xSize,
    // Индекс узла с большим Z

    k = j+1, // Индекс узла по диагонали

    n = i+1; // Индекс узла справа

    // Выбор координат 4-х вершин из контейнера
    float

    xi = m_cPoints [i] . х,

    yi = m_cPoints [i] .y,

    zi = m_cPoints [i] . z,

    xj = m_cPoints [ j ] .x,

    yj = m_cPoints [ j ] .y,

    zj = m_cPoints [ j ] .z,

    xk = m_cPoints [k] .x,

    yk = m_cPoints [k] . y,

    zk = m_cPoints [k] . z,

    xn = m_cPoints [n] .x,

    yn = m_cPoints [n] .y,

    zn = m_cPoints [n] . z,

    //=== Координаты векторов боковых сторон
    ах = xi-xn,
    ay = yi-yn,
    by = yj-yi,
    bz = zj-zi,

    //=== Вычисление вектора нормали
    vx = ay*bz,
    vy = -bz*ax,
    vz = ax*by,

    //=== Модуль нормали

    v = float (sqrt (vx*vx + vy*vy + vz*vz) ) ;

    //====== Нормировка вектора нормали

    vx /= v;
    vy /= v;
    vz /= v;

    //====== Задание вектора нормали

    glNormalSf (vx,vyfvz);

    // Ветвь создания несвязанных четырехугольников

    if (m_bQuad)

    {

    //====== Обход вершин осуществляется

    //=== в направлении против часовой стрелки
    glColorSf (0.2f, 0.8f, l.f);
    glVertex3f (xi, yi, zi);
    glColor3f <0.6f, 0.7f, l.f);
    glVertexSf (xj, уj, zj);
    glColorSf (0.7f, 0.9f, l.f);
    glVertexSf (xk, yk, zk);
    glColorSf (0.7f, 0.8f, l.f);
    glVertexSf (xn, yn, zn); }

    else

    // Ветвь создания цепочки четырехугольников

    {

    glColor3f (0.9f, 0..9f, l.Of);

    glVertexSf (xi, yi, zi);

    glColorSf (0.5f, 0.8f, l.0f);

    glVertexSf (xj, уj, zj);
    }
    }

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

    if (!m_bQuad)

    glEnd(); }

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

    if (m_bQuad) glEnd() ;

    //====== Закрываем список команд OpenGL

    glEndList ();
    }

    Для осмысления алгоритма надо учитывать, что количество узлов сетки вдоль того или иного направления (X или Z) на единицу больше количества промежутков (ячеек). Кроме того, надо иметь в виду, что при расчете освещения OpenGL учитывает направление нормали (перпендикуляра) к поверхности. Реалистичность изображения во многом достигается благодаря аккуратному вычислению нормалей. Нормаль является характеристикой вершины (узла сетки).

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

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

    // OpenGL.h : Declaration of the COpenGL

    #pragma once

    #include "resource.h" // main symbols

    #include

    #include "_IOpenGLEvents_CP.h"

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

    class CPointSD

    public:
    fldat x;
    float y;

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

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

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

    CPoint3D (float cl, float c2, float c3)

    x = с1;

    z = c2;

    у = сЗ;

    CPoint3D& operator=(const CPoint3D& pt)

    x = pt.x;
    z = pt. z ;
    У = pt.y;
    return *this;

    }

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

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

    p.ublic CQomObjectRootEx,

    public CStockPropImpKCOpenGL, IOpenGL>,

    public IPersistStreamInitImpl,

    public I01eControlImpl,

    public I01eObjectImpl,

    public I01eInPlaceActiveObjectImpl,

    public IViewObjectExImpl,

    public I01eInPlaceObjectWindowlessImpl,

    public ISupportErrorlnfo,

    public IConnectionPointContainerImpl,

    public CProxy_IOpenGLEvents,

    public IPersistStorageImpl,

    public ISpecifyPropertyPagesImpl,

    public IQuickActivateImpl,

    public IDataObjectImpl,

    public IProvideClassInfo2Impl<&CLSID_OpenGL,

    &_uuidof(_IOpenGLEvents), &LIBID_ATLGLLib>,

    public CComCoClass,
    public CComControl
    {
    public:

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

    для

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

    OLE COLOR

    m clrFillColor;

    //

    Цвет фона окна

    int

    m LightParamfll] ;

    //

    Параметры освещения

    int

    m xPos, m yPos;

    //

    Текущая позиция мыши

    HGLRC

    m hRC;

    //

    Контекст OpenGL

    HDC

    m hdc;

    //

    Контекст Windows

    GLfloat

    m AngleX;

    //

    Угол поворота вокруг оси X

    GLfloat

    m AngleY;

    //

    Угол поворота вокруг оси Y

    GLfloat

    m AngleView;

    //

    Угол перспективы

    GLfloat

    m fRangeX;

    //

    Размер объекта вдоль X

    GLfloat

    m fRangeY;

    //

    Размер объекта вдоль Y

    GLfloat

    m fRangeZ;

    //

    Размер объекта вдоль Z

    GLfloat

    m dx;

    //

    Квант смещения вдоль X

    GLfloat

    m dy;

    //

    Квант смещения вдоль Y

    GLfloat

    m xTrans;

    //

    Смещение вдоль X

    GLfloat

    m yTrans;

    //

    Смещение вдоль Y

    GLfloat

    m zTrans;

    //

    Смещение вдоль Z

    GLenum

    m FillMode;

    //

    Режим заполнения полигонов

    bool

    m_bCaptured;

    //

    Признак захвата мыши

    bool

    m bRightButton;

    //

    Флаг правой кнопки мыши

    bool

    m bQuad;

    //

    Флаг использования GL QUAD

    UINT

    m xSize;

    //

    Текущий размер окна вдоль X

    UINT

    m zSize;

    //

    Текущий размер окна вдоль Y

    //====== Массив вершин поверхности

    vector m_cPoints;

    //====== Функции, присутствовавшие в стартовой заготовке

    COpenGL();

    HRESULT OnDraw(ATL DRAWINFO& di);

    void OnFillColorChangedO ;

    DECLARE_OLEMISC_STATUS(OLEMISC_RECOMPOSEONRESIZE

    OLEMISC_CANTLINKINSIDE |

    OLEMISC_INSIDEOUT |

    OLEMISC_ACTIVATEWHENVISIBLE |

    OLEMISC_SETCLIENTSITEFIRST |

    DECLARE_REGISTRY_RESOURCEID(IDR_OPENGL)

    BEGIN_COM_MAP(COpenGL)

    COM_INTERFACE_ENTRY(IQpenGL)

    COM_INTERFACE_ENTRY(IDispatch)

    COM_INTERFACE_ENTRY(IViewObj ectEx)

    COM_INTERFACE_ENTRY(IViewObj ect2)

    COM_INTERFACE_ENTRY(IViewObj ect)

    COM_INTERFACE_ENTRY(I01eInPlaceObjectWindowless)

    COM_INTERFACE_ENTRY(I01eInPlaceObject)

    COM_INTERFACE_ENTRY2(IQleWindow,

    IQlelnPlaceObjectWindowless)

    COM_INTERFACE_ENTRY(lOlelnPlaceActiveObject)

    COM_INTERFACE_ENTRY(lOleControl)

    COM_INTERFACE_ENTRY(lOleObj ect)

    COM_INTERFACE_ENTRY(IPersistStreamInit)

    COM_INTERFACE_ENTRY2(IPersist, IPersistStreamlnit)

    COM_INTERFACE_ENTRY(ISupportErrorlnfo)

    COM_INTERFACE_ENTRY(IConnectionPointContainer)

    COM_INTERFACE_ENTRY(ISpecifyPropertyPages)

    COM_INTERFACE_ENTRY(IQuickActivate)

    COM_INTERFACE_ENTRY(IPersistStorage)

    COM_INTERFACE_ENTRY(IDataObject)

    COM_INTERFACE_ENTRY(IProvideClassInfo)

    COM_INTERFACE_ENTRY(IProvideClassInfo2) END_COM_MAP()

    BEGIN_PROP_MAP(COpenGL)

    PROP_DATA_ENTRY("_cx", m_sizeExtent. ex, VTJJI4)
    PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VTJJI4) PROP_ENTRY("FillColor",DISPID_FILLCOLOR, CLSID_StockColorPage)

    END_PROP_MAP()

    BEGIN_CONNECTION_POINT_MAP(COpenGL)

    CONNECTION_POINT_ENTRY(DIID_IQpenGLEvents)
    END_CONNECTION_POINT_MAP()

    BEGIN_MSG_MAP(COpenGL)

    CHAIN_MSG_MAP(CComControKCOpenGL>)

    DEFAULT_REFLECTION_HANDLER() END_MSG_MAP()

    //====== Поддержка интерфейса ISupportsErrorlnfо STDMETHOD(InterfaceSupportsErrorlnfo)(REFIID riid)
    {

    static const IID* arr[] =
    {
    &IID_IOpenGL,
    };

    for (int i=0; ixsizeof(arr)/sizeof(arr[0]); i++)
    {

    if (InlineIsEqualGUID(*arr[i], riid))

    return S_OK;
    }

    return S_FALSE;
    }

    //====== Поддержка интерфейса IViewObjectEx

    DECLARE_VIEW_STATUS(VIEWSTATUS_SOLIDBKGND | VIEWSTATUS_OPAQUE)

    //====== Поддержка интерфейса IQpenGL

    public: DECLARE_PROTECT_FINAL_CONSTRUCT()

    HRESULT FinalConstruct()
    {

    return S_OK;
    }

    void FinalRelease()

    { }

    //====== Экспонируемые методы

    STDMETHODIMP GetLightParams(int* pPos);
    STDMETHODIMP SetLightParam(short Ip, int nPos);
    STDMETHODIMP ReadData(void);

    //====== Новые методы класса

    //====== Установка параметров освещения

    void SetLight ();

    //====== Создание демонстрационного графика

    void DefaultGraphic();

    //====== Чтение файла с данными о графике

    bool DoRead(HANDLE hFile);

    // Заполнение координат точек графика по данным из буфера
    void SetGraphPoints(BYTE* buff, DWORD nSize);

    //====== Управление цветом фона окна

    void SetBkColor ();

    //== Создание изображения в виде списка команд OpenGL
    void DrawScene();
    };

    OBJECT ENTRY AUTO (_uuidof (OpenGL) , COpenGL)

    Обзор класса COpenGL

    Начальные строки кода класса должны показаться вам знакомыми, так как вы уже знаете, что мастер ATL ControlWizard предоставляет ко-классу множество родителей для обеспечения той функциональности, которая была заказана при создании стартовой заготовки. Макрос DECLARE_OLEMISC_STATUS задает набор битовых признаков, собранных в тип перечисления OLEMISC (miscellaneous — разнообразные, не принадлежащие одной стороне описания). Они описывают различные характеристики СОМ-объекта или класса. Контейнер может выяснить эти параметры с помощью метода lOleObject: :GetMiscStatus. Некоторые настройки попадают в специальный раздел реестра для сервера CLSiD\MiscStatus. Мы видим, что в заготовке присутствуют следующие биты:

  • OLEMISC_RECOMPOSEONRESIZE — сообщает контейнеру, что при изменении размеров окна объекта последний хочет не просто изменить пропорции, но и выполнить более сложную рекомпозицию. Отзывчивый контейнер должен запустить сервер и вызвать метод lOleObject: :SetExtent, передав новый размер окна;
  • OLEMISC_CANTLINKINSIDE — говорит о том, что после передачи объекта контейнером он может быть выбран, но при этом не может открыться в режиме для редактирования, то есть при помещении объекта в буфер обмена контейнер может предоставить свою связь (link), но не связь с объектом;
  • OLEMISC__INSIDEOUT — объект способен к активизации на месте (in place), но при этом не требуется изменять меню и инструментальную панель в рамках контейнера;
  • OLEMISC__ACTIVATEWHENVISIBLE — этот признак устанавливается одновременно с предыдущим и говорит о том, что объект хочет быть активным всякий раз, когда он становится видимым. Некоторые контейнеры могут и предпочитают игнорировать это указание;
  • OLEMISC_SETCLIENTSITEFIRST — этот признак характерен для всех средств управления (controls) и он говорит о том, что в качестве функции инициализации следует вызвать функцию lOleObject: : SetClientSite, которая позволяет определить свойства окружения (ambient properties), до того как будут загружена информация из хранилища (persistent storage). Далеко не все контейнеры способны учесть это указание.
  • Карты интерфейсов и свойств

    Далее по коду вы видите карту макросов COM map, которая скрывает механизм предоставления клиенту интерфейсов с помощью метода Querylnterf асе (vtable-интерфейсы). Как вы можете видеть, каркас сервера предоставляет и поддерживает достаточно много интерфейсов, не требуя от нас каких-либо усилий. За СОМ-картой следует карта свойств (см. BEGIN_PROP_MAP), которая хранит такие описания свойств, как индексы диспетчеризации типа DISPID, индексы страниц свойств (property pages) типа CLSID, а также индекс интерфейса IDispatch типа iID. Если обратиться к документации, то там сказано, что имя PROP_DATA_ ENTRY является именем функции, а не макросом, как естественно было бы предположить. Вызов этой функции делает данные, которые заданы параметрами, устойчивыми (persistent). Это означает, что если приложение-клиент сохраняет свой документ с внедренным в его окно элементом ActiveX, то размеры m_sizeExtent, заданные параметром функции, тоже будут сохранены. Немного ниже будет описано, как вставить в карту элемент, описывающий новую страницу свойств.

    Карта точек соединения

    Следующая карта BEGIN_CONNECTION_POINT_MAP описывает интерфейсы точек соединения (или захвата), которые характерны для соединяемых (connectable) СОМ-объектов. Так называются объекты, которые предоставляют клиенту исходящие (outgoing) интерфейсы.

    Примечание
    Примечание


    Интерфейсы, раскрываемые с помощью рассмотренного механизма Querylnterface, называются входящими (incoming), так как они входят в объект (запрашиваются) со стороны клиента. Как отмечает Kraig Brockschmidt (в уже упоминавшейся книге Inside OLE), входящие интерфейсы являются глазами и ушами СОМ-объекта, которые воспринимают сигналы из окружающего мира. Но некоторые объекты могут не только слушать, но и сказать нечто полезное. Это требует от клиента способности к диалогу. Двусторонний диалог подразумевает наличие исходящих (outgoing) интерфейсов и особого механизма общения, основанного на обработке событий (events), уведомлений (notifications) или запросов (requests).
    События и запросы сходны с Windows-сообщениями, которые также информируют окно о каком-то событии (WM_SIZE, WM_COMMAND) или запрашивают какие-то данные (WM_CTLCOLOR, WM_QUERYENDSESSION). Точки связи (connection points) предоставляются объектом для каждого исходящего из него интерфейса. Клиент, умеющий слушать, реализует эти интерфейсы с помощью объекта, называемого sink (сток, слив). Его можно представить себе в виде воронки, которую клиент подставляет для того, чтобы объект мог сливать в нее свои сообщения. С точки зрения стока исходящие (outgoing) интерфейсы являются входящими (incoming). Сток помогает клиенту слушать объект. Возможны варианты, когда одна воронка подставляется для восприятия интерфейсов от нескольких разных СОМ-объектов (multicasting) и когда один клиент предоставляет несколько воронок для восприятия интерфейсов от одного объекта.

    Каждая точка соединения СОМ-объекта поддерживает интерфейс iConnect-ionPoint. С помощью другого интерфейса — iConnectionPointContainer — объект рекламирует клиенту свои точки связи. Клиент пользуется интерфейсом IConnectionPointContainer для получения информации о наличии и количестве исходящих интерфейсов или, что то же самое, точек соединения. Узнав о наличии IConnectionPoint, клиент использует его для передачи объекту указателя на свой сток или нескольких указателей на несколько стоков. Большинство, и Kraig Brockschmidt в том числе, отмечают, что все это довольно сложно усвоить сразу, поэтому не переживайте, если потеряли нить рассуждений в данной информации. Постепенно все уляжется.

    Надо отметить, что в этой части СОМ используется наибольшее число жаргонных слов. Попробуем с их помощью коротко описать механизм, а также сценарий общения между клиентом и С О М-объектом при задействовании исходящих интерфейсов. Сначала объект беспомощен и не может сказать что-либо клиенту. Инициатива должна быть проявлена клиентом — контейнером СОМ-объекта. Он обычным путем запрашивает у сервера указатель на интерфейс IConnectionPointContainer, затем с помощью методов этого интерфейса (EnumConnectionPoints, FindConnectionPoint) получает указатель на интерфейс iConnectionPoint. Далее клиент использует метод Advise последнего интерфейса для того, чтобы передать объекту указатель на свой сток — воронку для слушания или слива сообщений. Начиная с этого момента объект имеет возможность разговаривать, так как он имеет воронку или указатель на интерфейс посредника в виде sink. Заставить замолчать объект может опять же клиент. Для этого он пользуется методом Unadvise интерфейса IConnectionPoint.
    Излишняя сложность всей конструкции объясняется соображениями расширяемости (extensibility). Соединяемые объекты могут усложняться независимо от точек соединения, а точки связи могут развиваться, не принося тревог соединяемым объектам. Меня подобный довод не убедил, но мы должны жить в этом мире, каков бы он ни был.

    Карта сообщений

    Карта сообщений, которая должна вызвать у вас ассоциацию с картой сообщений MFC, содержит незнакомый макрос CHAIN_MSG_MAP. Он перенаправляет необработанные сообщения в карту сообщений базового класса. Дело в том, что ATL допускает существование альтернативных карт сообщений. Они определяются макросами ALT_MSG_MAP. Тогда надо использовать макрос CHAIN_ MSG_MAP_ALT. Мы не будем обсуждать эту тему более подробно. Следующий макрос — DEFAULT_ REFLECTION_HANDLER — обеспечивает обработчик по умолчанию (в виде DefWindowProc) для дочерних окон элемента ActiveX, которые получают отражаемое (reflected) сообщение, но не обрабатывают его.

    Интерфейс ISupportsErrorlnfо

    Поддержка этого интерфейса проста. В методе interfaceSupportsErrorinfo имеется статический массив а г г, в котором хранятся адреса идентификаторов вновь создаваемых интерфейсов, пока он у нас один HD_iOpenGL. В этом же методе осуществляется пробег по всему массиву индексов и вызов функции inlinelsEqualGUio, которая пока не документирована, но ее смысл может быть выведен из ее имени.

    Интерфейс IViewObjectEx

    Этот интерфейс является расширением интерфейса iviewobject2. Он поддерживает обработку объектов непрямоугольной формы. Например, их улучшенную (flicker-free — не моргающую) перерисовку, проверку попадания курсора внутрь объекта, изменение размеров и полу прозрачность объектов. Моргание при перерисовке возникает из-за того, что перед ней стирается все содержимое окна. Бороться с этим можно, например, так: рисовать в bitmap (растровый рисунок), не связанный с экраном, а затем копировать весь bitmap на экран одной операцией. Нас эта проблема не волнует, так как мы будем использовать возможности OpenGL. Видимо, можно отказаться от услуг этого интерфейса при оформлении заказа у мастера ATL. Макрос DECLARE_VIEW_STATUS задает флаги прозрачности объекта, определенные в структуре VIEWSTATUS. По умолчанию предложен набор из двух неразлучных флагов:

  • VIEWSTATUS_SOLIDBKGND — использовать сплошной фон для окна в отличие от фона, основанного на узорной кисти (brush pattern);
  • VIEWSTATUS_OPAQUE — объект не содержит прозрачных частей, то есть полностью непрозрачен.
  • Макрос DECLARE_PROTECT_FINAL_CONSTRUCT защищает объект от удаления в случае, если внутренний (агрегированный) объект обнулит счетчик ссылок на наш объект. Метод CGomObjectRootEx: : FinalConstruct позволяет создать агрегированный объект с помощью функции CoCreatelnstance. Мы не будем пользоваться этой возможностью.

    Карта объектов

    В аналогичном проекте, созданном в рамках Visual Studio б, вы могли видеть карту объектов ов JECT_MAP, которая обеспечивает поддержку регистрации, инициализации и создания объектов. Карта объектов имеет привычную структуру:

    BEGIN_OBJECT_MAP

    OBJECT_ENTRY(CLSID_MyClass, MyClass)
    END_OBJECT_MAP()

    где макрос ов JECT_ENTRY вводит внутренний механизм отображений (тар) идентификаторов классов В их имена. При вызове функции CComModule; :RegisterServer она вносит в реестр записи, соответствующие каждому элементу в карте объектов. Здесь в рамках Studio.Net, вы видите другой макрос — OBJECT_ENTRY_AUTO, выполняющий сходную функцию, но при этом не нуждается в обрамлении из операторных скобок.

    Страницы свойств

    Страницы свойств
    Перед тем как мы начнем работать с окном СОМ-объекта, вводя в него реакции на управляющие воздействия, покажем, как добавить страницу свойств (property page) в уже существующий блок страниц объекта, который активизируется с помощью контекстного меню. Страница свойств является отдельным элементом управления, называемым Property Page, интерфейсы которого должны быть реализованы в рамках отдельного ко-класса. Такая структура позволяет нескольким ко-классам одновременно пользоваться страницами свойств, размещенными в общем СОМ DLL-сервере. Новый класс для поддержки страницы свойств помещается в сервер с помощью той же процедуры, которую мы использовали при вставке класса COpenGL, но при этом следует выбрать другой тип элемента управления. Вновь воспользуемся услугами мастера Studio.Net ATL Add Class.

  • Установите фокус на элемент ATLGL в дереве Solution Explorer и в контекстном меню выберите команду Add > Add Class, при этом важно, чтобы фокус стоял на имени проекта ATLGL
  • В окне диалога Add Class выберите категорию ATL, шаблон ATL Property Page и нажмите кнопку Open.
  • В окне мастера ATL Property Page выберите вкладку Names и в поле Short Name введите PropDlg.
  • Перейдите на вкладку Attributes и просмотрите допустимые установки, ничего в них не меняя.
  • Перейдите на вкладку Strings и в поле Title введите имя страницы Light, которое будет обозначено на вкладке (page tab). В поле Doc String введите строку Graphics Properties.
  • Нажмите кнопку Finish.
  • Просмотрите результаты. Прежде всего убедитесь, что в проекте появился новый класс CPropDlg, который поддерживает функциональность страницы свойств и окна диалога. Однако, запустив сервер и вызвав из контекстного меню его свойства, вы не увидите новой страницы. Там будут только те две страницы, которые были и до момента, как вы подключили поддержку страницы свойств. Для того чтобы новая страница действительно попала в блок страниц элемента, надо ввести новый элемент в карту свойств разрабатываемого элемента COpenGL. Откройте файл OpenGL.h и найдите в нем карту свойств. Она начинается строкой:

    BEGIN_PROP_MAP(COpenGL)

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

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

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

    BEGIN_PROP_MAP(COpenGL)

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

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

    PROP_ENTRY("FillColor", DISPID_FILLCOLOR, CLSID_StockColorPage)

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

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

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

    Примечание
    Примечание

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

    library ATLGLLib
    {

    importlib("stdole32.tlb");
    importlib("stdole2.tlb") ;

    [

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

    helpstring("_IOpenGLEvents Interface")
    ]

    dispinterface _IOpenGLEvents
    {

    properties:

    methods:
    };

    [

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

    helpstringC'OpenGL Class")
    ]

    coclass COpenGL
    {

    [default] interface IQpenGL;

    [default, source] dispinterface _IOpenGLEvents;
    };

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

    [

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

    helpstring("_IGraphPropEvents Interface")
    ]

    dispinterface _IGraphPropEvents
    {

    properties:

    methods:
    };

    [

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

    helpstring("GraphProp Class")
    ]

    coclass PropDlg
    {

    interface lUnknown;

    [default, source] dispinterface _IGraphPropEvents;
    };

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

    CPropDlg()
    {

    m_dwTitleID = IDSJTITLEPropDlg;

    m_dwHelpFileID = IDS_HELPFILEPropDlg;

    m_dwDocStringID = IDS_DOCSTRINGPropDlg;
    }

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

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

    STDMETHOD(Apply)(void)

    {

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

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

    // Do something interesting here
    // ICircCtl* pCirc;

    //m_ppUnk[i]->QueryInterface(IID_ICircCtl, (void**)SpCirc)
    // pCirc->put_Caption(CComBSTR("smth special"));
    // pCirc->Release();
    }

    m_bDirty = FALSE;

    return S__OK;
    }

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

    Примечание
    Примечание

    Имя ICircCtl, которое присутствует в подсказке, не имеет отношения к нашему проекту. Оно связано с учебным примером по созданию элементов управления с помощью библиотеки ATL. Вы можете увидеть этот пример в MSDN (Visual C++ Tutorials > Creating the Circle Control).
    Переменная m_bDirty используется каркасом в качестве флага доступности кнопки Apply. Если m_bDirt у == FALSE; то кнопка недоступна. Она тотчас же должна стать доступной, если пользователь страницы диалога свойств введет изменения в органы управления на лице диалога. Конечно, этим состоянием управляет разработчик, то есть мы с вами.

    Требования OpenGL

    Требования OpenGL

    Вместо тестового изображения с надписью ATL 4.0, которым мы научились кое-как управлять, поместим в окно СОМ-объекта OpenGL-изображение поверхности в трехмерном пространстве. Точнее, мы хотим дать клиенту нашего СОМ-объекта возможность пользоваться всей той функциональностью, которая была разработана в уроке 7. Вы помните, что изображение OpenGL может быть создано в окне, которое прошло специальную процедуру подготовки. Необходимо создать и сделать текущим контекст передачи OpenGL (HGRC). Вы также помните, что подготовку контекста надо рассматривать как некий обязательный ритуал, в котором порядок действий определен. Повторим его:

  • установка стиля окна;
  • обработка сообщения WM_ERASEBACKGROUND и отказ от стирания фона;
  • установка pixel-формата;
  • создание контекста устройства (нос) и контекста передачи (HGLRC);
  • специфическая обработка сообщения WM_SIZE;
  • обработка сообщения WM_PAINT;
  • освобождение контекстов при закрытии окна.
  • Чтобы использовать функции библиотеки OpenGL, надо подключить их к проекту. На этапе компоновки они будут интегрированы в коды СОМ-сервера.

  • В окне Solution Explorer поставьте фокус на строку с именем проекта ATLGL и нажмите кнопку Properties, которая расположена на панели инструментов этого окна.
  • В левом окне диалога ATLGL Property Pages найдите и выберите ветвь дерева Linker.
  • В раскрывшемся поддереве выберите ветвь Input и перейдите в строку Additional Inputs в таблице правого окна.
  • Поставьте фокус во вторую колонку этой строки и в конец существующего текста ячейки добавьте, не стирая содержимое ячейки, имена подключаемых библиотек OPENGL32.LIB GLU32.LIB, не забыв о разделяющих пробелах. Нажмите ОК.
  • В конец файла библиотечных заголовков stdafx.h добавьте строки:
  • #include

    #include
    #include

    При работе с трехмерными координатами мы пользовались вспомогательным классом CPoint3D, который здесь нам тоже понадобится. Нужны будут и все переменные, которые были использованы ранее для управления сценой OpenGL. Там, если вы помните, был контейнер STL типа vector для хранения точек изображения. Использование контейнеров требует подключения соответствующих файлов заголовков, поэтому вставьте в конец файла stdafx.h следующие строки:

    #include using namespace std;

    Так как мы собираемся демонстрировать в окне OpenGL графики функций, диапазон изменения которых нам заранее не известен, то следует использовать предварительное масштабирование координат точек графика. Нам надо знать габариты изображаемого объекта и для упрощения этой задачи введем вспомогательную глобальную функцию корректировки экстремумов:

    inline void MinMax (float d, floats Min, floats Max)
    {

    if (d > Max) Max = d;
    else if (d < Min)

    Min = d;
    }

    Описатель inline сообщает компилятору, что функцию можно не реализовывать в виде отдельной процедуры, а ее тело желательно вставлять в точки вызова, с тем чтобы убрать код обращения к стеку. Окончательное решение при этом остается за компилятором.

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

    Трехмерная графика в проекте ATL
  • Требования OpenGL
  • Введение обработчиков сообщений Windows
  • Управление цветом фона
  • Подготовка сцены OpenGL
  • Файловые операции
  • Установка освещения
  • Реализация методов интерфейса
  • Страницы свойств
  • Взаимодействие классов
  • Управление объектом с помощью мыши
  • Создание контейнера на базе MFC
  • Класс-оболочка
  • В этом уроке мы продолжим разработку DLL-модуля, который после регистрации в системе в качестве СОМ-объекта позволит любому другому клиентскому приложению, обладающему свойствами контейнера объектов СОМ использовать его для отображения в контексте OpenGL трехмерного графика функции, заданной произвольным массивом чисел. Данные для графика СОМ-объект берет из файла, на который указывает пользователь клиентского приложения. Кроме этого, объект предоставляет клиенту возможность перемещения графика вдоль трех пространственных осей, вращения вокруг вертикальной и горизонтальной осей и просмотра как в обычном, так и скелетном режиме. Регулируя параметры освещения поверхности, пользователь может добиться наибольшей реалистичности изображения, то есть усилить визуальный эффект трехмерного пространства на плоском экране.
    Графики могут представлять результаты расчета какого-либо физического поля, например поверхности равной температуры, давления, скорости, индукции, напряжения и т. д. в части трехмерного пространства, называемой расчетной областью. Пользователь объекта должен заранее подготовить данные и записать их в определенном формате в файл. Объект по команде пользователя считывает данные, нормирует, масштабирует и изображает в своем окне, внедренном в окно приложения-клиента. Пользователь, манипулируя мышью, управляет местоположением и вращением графика, а открыв стандартный диалог Properties, изменяет другие его атрибуты.

    ATL (Active Template Library) — это библиотека шаблонов функций и классов, которая разработана с целью упрощения и ускорения разработки СОМ-объектов. Несмотря на заявления о том, что ATL не является альтернативой MFC, а лишь дополняет ее, побудительной причиной разработки этой библиотеки был тот факт, что объекты СОМ, разработанные с помощью MFC, и внедренные в HTML-документ, работали слишком медленно. Наследование от cobject и все те удобства, которые оно приносит, обходятся слишком дорого в смысле быстродействия, и в условиях web-страницы объекты MFC-происхождения проигрывают объектам, разработанным с помощью COM API. В библиотеке ATL не используется наследование от cobject и некоторые другие принципы построения классов, характерные для MFC. За счет этого удалось повысить эффективность работы СОМ-объектов и ускорить их функционирование даже в условиях web-страниц. Пользуясь справкой (Help), вы, наверное, видели, что многие оконные методы реализованы не только в классе cwnd, но и в классе cwindow. Последний является классом из иерархии библиотеки ATL, и именно он является главной фигурой при разработке окон СОМ-объектов.

    Управление цветом фона

    Управление цветом фона

    Возможность изменять цвет фона окна OpenGL удобно реализовать с помощью отдельного метода класса:
    void COpenGL::SetBkColor()
    {
    //====== Расщепление цвета на три компонента

    GLclampf red = GetRValue(m_clrFillColor)/255 . f,

    green = GetGValue(m_clrFillColor)/255.f,

    blue = GetBValue(m_clrFillColor)/255.f;

    //====== Установка цвета фона (стирания) окна

    glClearColor (red, green, blue, O.f);

    //====== Непосредственное стирание

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    }

    Вызов этого метода должен происходить при первоначальном создании окна, то есть внутри OnCreate, и при каждом изменении стандартного свойства (stock property) в окне свойств. Первое действие мы уже выполнили, а второе необходимо сделать, изменив тело функции OnFillColorChanged:

    void COpenGL::OnFillColorChanged()
    {
    //====== Если выбран системный цвет,

    if (m_clrFillColor & 0x80000000)

    //====== то выбираем его по индексу

    m_clrFillColor = GetSysColor(m_clrFillColor & Oxlf);

    //====== Изменяем цвет фона окна OpenGL

    SetBkColor ();
    }

    Управление с помощью объекта классаоболочки

    Управление с помощью объекта класса-оболочки

    Для управления внедренным элементом ActiveX надо ввести в существующий диалоговый класс CTestGLDlg объект (переменную типа) класса-оболочки. Этот шаг тоже автоматизирован в Studio.Net, так как введение объекта влечет сразу несколько строк изменения кода.

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

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

  • объявление переменной COpenGL m_ctrl; в файле заголовков TestGLDlg.h;
  • вызов функции DDX_Control(pDX, IDC_OPENGL, m_ctrl), связывающей элемент управления в окне диалога с переменной m_ctrl. Этот вызов вы найдете в теле функции CTestGLDlg::DoDataExchange;
  • Для обеспечения видимости вставьте в начало файла TestGLDlg.h директиву:

    #include "opengl.h"

    В конец файла Stdafx.h вставьте директивы подключения заголовков библиотеки OpenGL:

    #include
    // Будем пользоваться OpenGL
    #include

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

  • OnClickedFilename — нажата кнопка IDC_FILENAME,
  • OnCiickedBkcir — нажата кнопка IDC_BKCLR,
  • OnSelchangeFill — изменился выбор в списке IDC_FILL,
  • OnClickedQuads — нажата кнопка IDC_QUADS,
  • OnHScroll — изменилась позиция ползунка IDC_XPOS,
  • OnClickedStrips — нажата кнопка IDC_STRIPS.
  • Ниже мы приведем тела этих функций, а сейчас отметим, что все они пользуются услугами класса-оболочки для прямого вызова методов СОМ-сервера. Однако, как вы могли заключить из рассмотрения кодов класса COpenGL, на самом деле вызов будет происходить с помощью интерфейса IDispatch, а точнее его метода Invoke. Функция cwnd: : invokeHelper, вызов которой вы видите во всех методах COpenGL, преобразует параметры к типу VARIANTARG, а затем вызывает функцию Invoke. Если происходит отказ, то Invoke выбрасывает исключение.
    В диалоговом классе мы попутно произвели упрощения, которые связаны с удалением ненужных функций OnPaint и OnQueryDragicon. Эти изменения обсуждались при разработке приложения Look. Во избежание недоразумений, которые могут возникнуть в связи с многочисленным ручным редактированием, приведем коды как декларации, так и реализации класса CTestGLDlg:
    //=== Декларация диалогового класса (Файл TestGLDlg.h)
    #include "opengl.h"
    #pragma once

    class CTestGLDlg : public CDialog

    {

    public:

    CTestGLDlg(CWnd* p = NULL);

    enum
    {
    IDD = IDD_TESTGL_DIALOG
    };

    //======= Объект класса-оболочки

    COpenGL m_Ctrl;

    //======= Запоминаем способ изображения

    BOOL m_bQuads;

    //======= Реакции на регуляторы в окне диалога

    void OnSelchangeFill(void);

    void OnClickedFilename(void);

    afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);

    void OnCiickedBkcir(void);

    void OnClickedQuads(void);
    void OnClickedStrips(void);

    protected:

    virtual
    void DoDataExchange(CDataExchange* pDX) ;
    virtual BOOL OnlnitDialog();

    afx_msg void OnSysCommand(UINT nID, LPARAM IParam);
    DECLARE_MESSAGE_MAP()

    };

    В файл реализации методов класса мы кроме функций обработки сообщений от элементов управления вставили код начальной установки этих элементов. Для этой цели нам опять понадобилась связь с сервером, которую обеспечивает объект m_ctrl класса-оболочки. Характерным моментом является то, что обрабатываем событие WM_HSCROLL, которое поступает окну диалога, вместо того чтобы обработать уведомляющее событие NM_RELEASEDCAPTURE, которое идет от элемента типа Slider Control. Такая тактика позволяет реагировать на управление ползунком клавишами, а не только мышью:

    #include "stdafx.h"
    #include "TestGL.h"

    #include "TestGLDlg.h"

    #ifdef _DEBUG

    #define new DEBUG_NEW

    #undef THIS_FILE

    static char THIS_FILE[] = _FILE_;

    #endif

    //====== Пустое тело конструктора

    CTestGLDlg::CTestGLDlg(CWnd* p) : CDialog(CTestGLDlg::IDD, p){}

    void CTestGLDlg::DoDataExchange(CDataExchange* pDX) {

    //====== Связывание переменной с элементом

    DDX_Control(pDX, IDCJDPENGL, m_Ctrl);

    CDialog::DoDataExchange(pDX);
    }

    //====== Здесь мы убрали ON_WM_PAINT и т. д.

    BEGIN_MESSAGE_MAP(CTestGLDlg, CDialog) ON_WM_SYSCOMMAND()

    //
    }
    }
    AFX_MSG_MAP

    ON_CBN_SELCHANGE(IDC_FILL, OnSelchangeFill)
    ON_BN_CLICKED(IDC_FILENAME, OnClickedFilename)
    ON_WM_HSCROLL()

    ON_BN_CLICKED(IDC_BKCLR, OnClickedBkclr)
    ON_BN_CLICKED(IDC_QUADS, OnClickedQuads)
    ON_BN_CLICKED(IDC_STRIPS, OnClickedStrips)

    END_MESSAGE_MAP()

    //===== CTestGLDlg message handlers

    BOOL CTestGLDlg::OnInitDialog()
    {
    //====== Добываем адрес меню управления окном

    CMenu* pSysMenu = GetSystemMenu(FALSE);

    if (pSysMenu)

    {

    //====== Добавляем команду About

    pSysMenu->AppendMenu(MF_SEPARATOR);
    pSysMenu->AppendMenu(MF_STRING,

    IDM_ABOUTBOX,"About...");
    }

    //====== Загружаем стандартный значок

    HICON hMylcon = ::LoadIcon(0,(char*)IDI_WINLOGO);
    Setlcon(hMylcon, TRUE); // Set big icon Setlcon(hMylcon, FALSE);
    // Set small icon

    CDialog::OnInitDialog();

    //====== Начальная установка элементов

    CComboBox *pBox = (CComboBox*)GetDlgltem(IDC_FILL);
    pBox->AddString("Points"); pBox->AddString("Lines");
    pBox->AddString("Fill"); pBox->SetCurSel (2);

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

    m_Ctrl.GetQuad(&m_bQuads);

    WPARAM w = m_bQuads ? BST_CHECKED : BST_UNCHECKED;

    //===== Устанавливаем состояние переключателя
    GetDlgltem(IDC_QUADS)->SendMessage(BM_SETCHECK, w, 0);
    w = m_bQuads ? BST_UNCHECKED : BST_CHECKED;
    GetDlgltem(IDC_STRIPS)->SendMessage(BM_SETCHECK, w, 0);

    return TRUE;
    }

    void CTestGLDlg::OnSysCommand(UINT nID, LPARAM iParam)
    {

    if ((nID S OxFFFO) == IDM_ABOUTBOX)

    {

    CDialog(IDD_ABOUTBOX).DoModal();

    }

    else

    {

    CDialog::OnSysCommand(nID, IParam);

    }
    }

    //====== Выбор из списка типа Combo-box

    void CTestGLDlg::OnSelchangeFill(void) "'*
    {
    DWORD sel = ((CComboBox*)GetDlgltem(IDC_FILL))->GetCurSel();

    sel = sel==0 ? GL_POINT : sel==l ? GL_LINE

    : GL_FILL;

    m_Ctrl.SetFillMode(sel);
    }

    //==== Нажатие на кнопку запуска файлового диалога

    void CTestGLDlg::OnClickedFilename(void)

    {

    m_Ctrl.ReadData();
    }

    //====== Реакция на сдвиг ползунка

    void CTestGLDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
    {

    //====== Выясняем текущую позицию, которая не во

    //====== всех случаях отражена в параметре nPos

    nPos = ((CSliderCtrl*)GetDlgItem(IDC_XPOS))->GetPos() ;

    m_Ctrl.SetLightParam (0, nPos);
    }

    //====== Запускаем стандартный диалог

    void CTestGLDlg::OnClickedBkclr(void)
    {

    DWORD clr = m_Ctrl.GetFillColor() ;

    CColorDialog dig (clr);

    dig.m_cc.Flags |= CC_FULLOPEN;

    if (dlg.DoModal()==IDOK)

    {

    m_Ctrl.SetFillColor(dlg.m_cc.rgbResult);

    }
    }

    //====== Запоминаем текущее состояние и

    //====== вызываем метод сервера

    void CTestGLDlg::OnClickedQuads(void)
    {

    m_Ctrl.SetQuad(m_bQuads = TRUE);
    }

    void CTestGLDlg::OnClickedStrips(void)
    {

    m_Ctrl.SetQuad(m_bQuads = FALSE);
    }
    В настоящий момент вы можете запустить приложение, которое должно найти и запустить DLL-сервер ATLGL, генерирующий изображение по умолчанию и демонстрирующий его в окне внедренного элемента типа ActiveX. Сервер должен достаточно быстро реагировать на изменение регулировок органов управления клиентского приложения.
    Подведем итог. В этом уроке мы научились:
  • вносить функциональность окна OpenGL в окно, управляемое ATL-классом CWindow;
  • добавлять с помощью Studio.Net новые методы в интерфейс, представляемый ко-классом;
  • учитывать особенности обработки сообщений Windows в рамках ATL;
  • управлять контекстом передачи OpenGL, связанным с окном внедренного СОМ-объекта;
  • создавать приложение-контейнер на базе MFC и пользоваться услугами класса-оболочки для управления СОМ-объектом.


  • Установка освещения

    Установка освещения
    Параметры освещения будут изменяться с помощью регуляторов, которые мы разместим на новой странице блока Property Pages. Каждую новую страницу этого блока принято реализовывать в виде отдельного интерфейса, раскрываемого специальным объектом (ко-классом) ATL. Однако уже сейчас мы можем дать тело вспомогательной функции SetLight, которая устанавливает параметры освещения, подобно тому как это делалось в уроке, где говорили о графике в рамках MFC. Параметры освещения будут храниться в массиве m_LightParam, взаимо-действовующем с диалогом, размещенным на новой странице свойств:

    void COGCOpenGLView::SetLight()
    {

    //====== Обе поверхности изображения участвуют

    //====== при вычислении цвета пикселов при

    //====== учете параметров освещения

    glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, 1) ;

    //====== Позиция источника освещения

    //====== зависит от размеров объекта

    float fPosf] =
    {

    (m_LightParam[0]-50)*m_fRangeX/100,

    (m_LightParam[l]-50)*m_fRangeY/100,

    (m_LightParam[2]-50)*m_fRangeZ/100,

    l.f
    };

    glLightfv(GL__LIGHTO, GL_POSITION, fPos);

    //====== Интенсивность окружающего освещения

    float f = m_LightParam[3]/100. f ;
    float fAmbient[4] = { f, f, f, O.f };
    glLightfv(GL_LIGHTO, GL_AMBIENT, fAmbient);

    //====== Интенсивность рассеянного света

    f = m_LightParam[4]/lOO.f ;

    float fDiffuse[4] = { f, f, f, O.f } ;

    glLightfv(GL_LIGHTO, GL_DIFFUSE, fDiffuse);

    //====== Интенсивность отраженного света

    f = m_LightParam[5]/l00.f;

    float fSpecular[4] = { f, f, f, 0. f } ;

    glLightfv(GL_LIGHTO, GL_SPECULAR, f Specular.) ;

    //====== Отражающие свойства материала

    //===== для разных компонентов света

    f = m_LightParam[61/100.f;

    float fAmbMat[4] = { f, f, f, O.f };

    glMaterialfv(GL_FRONT_AND_BACK, GL__AMBIENT, fAmbMat);

    f = m_LightParam[7]/l00.f;

    float fDifMat[4] = {- f, f, f, l.f } ;

    glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, fDifMat);

    f = m_LightParam[8]/lOO.f;

    float fSpecMat[4] = { f, f, f, 0.f };

    glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, fSpecMat);

    //======= Блесткость материала

    float fShine = 128 * m_LightParam[9]/100.f;
    glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, fShine);

    //======= Излучение света материалом

    f = m_LightParam[10]/lOO.f;
    float fEmission[4] = { f, f, f, O.f };

    glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, fEmission);
    }
    Параметры освещения
    Данные о том, как должна быть освещена сцена, мы будем получать из диалоговой вкладки свойств, которую создадим позже, но сейчас можем дать коды методов обмена данными, которые являются частью интерфейса lOpenGL:

    STDMETHODIMP COpenGL::GetLightParams(int* pPos)
    {
    //======= Проход по всем регулировкам

    for (int 1=0; i
    //======= Заполняем транспортный массив pPos

    pPos[i] = m_LightParam[i];
    return S_OK;
    }

    STDMETHODIMP COpenGL: : SetLightParam (short lp, int nPos)

    //====== Синхронизируем параметр 1р и устанавливаем

    //====== его в положение nPos

    m_LightParam[lp] = nPos;

    //==== Перерисовываем окно с учетом изменений
    FireViewChange ();
    return S_OK;
    }

    Метод CComControl: : FireViewChange уведомляет контейнер, что объект хочет перерисовать все свое окно. Если объект в данный момент неактивен, то уведомление с помощью указателя m_spAdviseSink поступает в клиентский сток (sink), который мы рассматривали при обзоре точек соединения.

    В данный момент вы можете построить DLL и посмотреть, что получилось, запустив тестовый контейнер. Однако, как это часто бывает в жизни программиста, мы не увидим ничего, кроме пустой рамки объекта. В таком состоянии можно остаться надолго, если не хватает квалификации и опыта отладки СОМ DLL-серверов. Сразу не видны даже пути поиска причины отказа. Никаких грубых промахов вроде бы не совершали. Процесс создания окна внедренного объекта происходит где-то за кадром. Опытный читатель, возможно, давно заметил неточность, которая закралась на самой начальной стадии создания заготовки ATL Control, но если опыта или знаний недостаточно, то надо все начинать заново, или рассматривать работающие примеры и скрупулезно сравнивать код. Здесь я потратил пару мучительных дней, видимо, по своей глупости, но все-таки нашел причину отказа. Она, как это тоже часто бывает, оказалась очень простой и очевидной. Мы забыли установить один флажок при создании заготовки ко-класса, который устанавливает в TRUE переменную:

    CComControl::m_bWindowOnly

    Наш класс GOpenGL, конечно же, унаследовал эту переменную. Она указывает СОМ, что элемент ActiveX должен создавать окно, даже если контейнер поддерживает элементы, не создающие окон. Приведем оригинальный текст: «m_bWindowOnly — Flag indicating the control should be windowed, even if the container supports win-do wless controls». Для исправления ситуации достаточно вставить в конструктор класса COpenGL такую строку:

    m_bWindowOnly = TRUE;

    После этого вы должны увидеть окно нашего ActiveX элемента, а в нем поверхность, вид которой показан на Рисунок 9.1.

    Реализация методов интерфейса
    Методы, обозначенные в интерфейсе IOреnсb, будут вызываться из клиентского приложения либо через IDispatch, либо с помощью страницы свойств, которую мы вскоре создадим. В любом случае, эти методы должны либо получить параметр настройки изображения и перерисовать его с учетом настройки, либо вернуть текущее состояние запрашиваемого параметра настройки:



    Вид новой вставки в блоке страниц свойств элемента ActiveX

    Рисунок 9.2. Вид новой вставки в блоке страниц свойств элемента ActiveX

    Вид новой вставки в блоке страниц свойств элемента ActiveX На рисунке показано окно диалога в активном состоянии, но вам еще предстоит поработать, чтобы довести его до этого вида. Здесь очень важно не торопиться и быть внимательным. Опыт преподавания в MS Authorized Educational Center (www.Avalon.ru) подтверждает, что большая часть ошибок вносится на стадии работы с ресурсами. Визуальные редакторы расслабляют внимание, и ошибки появляются там, где вы меньше всего их ждете.

    В основных чертах окно имеет тот же облик, что и окно диалога по управлению освещением сцены, разработанное ранее (в MFC проекте). Но здесь есть два новых элемента, функциональность которых ранее была спрятана в командах меню. Так как в рамках этого проекта мы не имеем меню, то нам пришлось использовать элементы управления, сосредоточенные в нижней части окна диалоговой вставки. Во-первых, не забудьте, что справа от каждого ползунка вы должны расположить элемент типа static Text, в окне которого будет отражено текущее положение движка в числовой форме.
    Кнопка Выбор файла, как и ранее, позволяет пользователю открыть файловый диалог и выбрать файл с данными для нового изображения. Выпадающий список Заполнение позволяет выбрать режим изображения полигонов (GL_FILL, GL_POINT или GL_LINE), а кнопка Quads/Strip изменяет режим использования примитивов при создании поверхности. Идентификаторы элементов управления мы сведем в табл. 9.1.

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

    Элемент

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

    / Диалог

    IDD_PROPDLG

    Ползунок Общая в группе Освещенность

    IDC_AMBIENT

    Ползунок Рассеянная в группе Освещенность

    IDC_DIFFUSE

    Ползунок Отраженная в группе Освещенность

    IDC_SPECULAR

    Text справа от Общая в группе Освещенность

    IDC_AMB_TEXT

    Text справа от Рассеянная в группе Освещенность

    IDC_DIFFUSE_TEXT

    Text справа от Отраженная в группе Освещенность

    IDC_SPECULAR_TEXT

    Ползунок Общая в группе Материал

    IDC_AMBMAT

    Ползунок Рассеянная в группе Материал

    IDC_DIFFMAT

    Ползунок Отраженная в группе Материал

    IDC.SPECMAT

    Text справа от Общая в группе Материал

    IDC_AMBMAT_TEXT

    Text справа от Рассеянная в группе Материал

    IDC_DIFFMAT_TEXT

    Text справа от Отраженная в группе Материал

    IDC_SPECMAT_TEXT

    Ползунок Блестскость

    IDC_SHINE

    Ползунок Эмиссия

    IDC.EMISSION

    Text справа от Блестскость

    IDC_SHINE_TEXT

    Text справа от Эмиссия

    IDC_EMISSION_TEXT

    Ползунок X

    IDC_XPOS

    Ползунок Y

    IDC.YPOS

    Ползунок Z

    IDC_ZPOS

    Text справа от X

    IDC_XPOS_TEXT

    Text справа от¥

    IDC_YPOS_TEXT

    Text справа от Z

    IDC_ZPOS_TEXT

    Выпадающий список Заполнение

    IDC_FILLMODE

    Кнопка Quads

    IDC.QUADS

    Кнопка Выбор файла

    IDC_FILENAME

    Вместо кнопки Quads просится пара переключателей (radio button) Quads/Strip. Сначала я так и сделал, но потом, к сожалению, пришлось отказаться из-за сложностей введения отклика реакции или уведомления, на выбор, произведенный в группе переключателей. Они обусловлены несовершенством бета-версии Studio.Net. Если вы впервые устанавливаете группу переключателей (radio buttons), то вам следует знать, что группа Quads/Strip будет работать правильно, если числовые значения идентификаторов составляющих ее элементов следуют подряд и (только) для первого переключателя установлено свойство Group. Для второго этот флаг должен быть снят. Если вы вставляете еще одну группу, то картина должна повториться. Первый переключатель должен иметь свойство Group в положении True, а остальные (если их много) — нет.

    Для того чтобы просмотреть числовые значения идентификаторов, следует поставить фокус на элемент IDD_PROPDLG в дереве ресурсов (в окне Resource View) и вызвать контекстное меню. Затем надо выбрать команду Resource Symbols. Появится диалог со списком всех идентификаторов, которые хранятся в файле resource.h. Не следует редактировать этот файл вручную.
    Примечание
    Примечание

    Изменять числовые значения идентификаторов следует с большими предосторожностями, так как ошибки на этом этапе могут внести трудно распознаваемые отказы и нестабильную работу приложения. Надо сказать, что отслеживание корректности числовых значений идентификаторов всегда было слабым местом как Visual Studio, так и среды разработки Borland. Беру на себя смелость предположить, что уйма времени была затрачена разработчиками всех стран на поиск ошибок такого рода, так как сам потратил много усилий и времени пока не понял, что легче уничтожить ресурс и создать заново, чем пытаться найти новый диапазон числовых значений, который не затронет другие идентификаторы.
    Если, несмотря на предостережения, вам захочется изменить числовое значение какого-либо идентификатора, то можете это сделать в окне Properties.

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

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

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

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

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

    Введение методов в интерфейс IOpenGL

    Введение методов в интерфейс IOpenGL
    На этом этапе важно решить, какие данные (свойства) и методы класса будут экспонироваться СОМ-объектом, а какие останутся в качестве служебных, для внутреннего пользования. Те методы и свойства, которые будут экспонированы, должны быть соответствующим образом отражены в IDL-файле. Те, которые нужны только нам, останутся внутри сервера. Для примера введем в число экспонируемых методов функцию GetLightParams, которая определяет действующие параметры освещения.

  • Поставьте фокус на строку с именем интерфейса lOpenGL в окне CLassView и вызовите контекстное меню.
  • Выберите команду Add > Add Method В окне мастера Add Method Wizard введите в поле Method Name имя метода GetLightParams. В поле Parameter Name введите имя параметра pPos, в поле Parameter Type: — тип параметра int*, задайте атрибут параметра, установив флажок out, и нажмите кнопку Add.
  • Нажмите кнопку Finish.
  • Проанализируйте изменения, которые появились в IDL-файле, в файле OpenGLh и в файле OpenGLcpp. В первом из перечисленных файлов появилось новое, уточненное описание метода интерфейса1:

    interface lOpenGL : IDispatch
    {

    [propput, bindable, requestedit, id(DISPID_FILLCOLOR)]

    HRESULT FillColor([in]OLE_COLOR clr);

    [propget, bindable, requestedit, id(DISPID_FILLCOLOR)]

    HRESULT FillColor([out, retval]OLE_COLOR* pclr);

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

    HRESULT GetLightParams([out] int* pPos);
    };

    в файле заголовков появилась строка декларации метода ко-класса, который реализует функциональность интерфейса:

    STDMETHODIMP GetLightParams(int* pPos);

    и, наконец, в файле реализации ко-класса появилась стартовая заготовка тела метода:

    STDMETHODIMP COpenGL::GetLightParams(int *pPos)
    {
    // TODO: Add your implementation code here

    return S_OK;
    }

    Повторите описанные действия и введите в интерфейс еще один метод SetLightParam, который изменяет один из параметров освещения сцены OpenGL. При задании параметров этого метода добейтесь такого описания в окне Parameter List:

    [in] short lp [in] int nPos;

    Введите в состав интерфейса еще один метод ReadData, на сей раз без параметров. Он будет реагировать на кнопку и производить чтение файла с данными о новом графике. Для управления обликом поверхности графика нам понадобятся две пары методов типа get-set. Введите в интерфейс следующие методы:

  • GetFillMode с параметром [out] DWORD* pMode;
  • SetFillMode С параметром [in] DWORD nMode;
  • GetQuad с параметром [out] BOOL* bQuad;
  • SetQuad с параметром [in] BOOL bQuad.
  • Найдите новые методы в IDL-файле и убедитесь, что мастер автоматически пронумеровал методы (1,2,...), присвоив им индексы типа DISPID:

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

    HRESULT GetLightParams([out] int* pPos);
    [id(2), helpstring("method SetLightParam")]

    HRESULT SetLightParam([in] short Ip, [in] int nPos);
    [id(3), helpstring("method ReadData")]

    HRESULT ReadData(void);
    [id(4), helpstring("method GetFillMode")]

    HRESULT GetFillMode([out] DWORD* pMode);
    [id(5), helpstring("method SetFillMode")]

    HRESULT SetFillMode([in] DWORD nMode);
    [id(6), helpstring("method GetQuad")]

    HRESULT GetQuad([out] BOOL* bQuad);
    [id(7), helpstring("method SetQuad")]

    HRESULT SetQuad([in] BOOL bQuad);

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

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

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

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

    CComQIPtr р(m_ppUnk[i]) ;

    он настраивается на нужный нам интерфейс, и далее мы работаем с удобствами, не думая о функциях Querylnterface, AddRef и Release. При выходе из области действия объекта р класса CGomQiPtr освобождение интерфейса произойдет автоматически.
    Для обмена с окном диалоговой вставки введите в protected-секцию класса CPropDlg массив текущих позиций регуляторов и переменную для хранения текущего режима изображения полигонов:
    protected:

    int m_Pos[11]; BOOL m_bQuad;

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

    ZeroMemory (m_Pos, sizeof(m_Pos));

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

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

    LPARAM IParam, BOOL& bHandled)
    {

    _super::OnInitDialog(uMsg, wParam, IParam, bHandled);
    return 1;
    }

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

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

    LPARAM IParam, BOOL& bHandled)

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

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

    CComQIPtr p(m_ppUnk[0]);

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

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

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

    DWORD mode;

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

    {

    ShowError();

    return 0;
    }

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

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

    HWND hwnd = GetDlgItem(IDC_FILLMODE);

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

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

    // Выбираем текущую позицию списка в соответствии
    // со значением, полученным из COpenGL WPARAM
    w = mode == GL_POINT ? 0

    : mode == GL_LINE ?1:2;
    SendMessage(hwnd, CB_SETCURSEL, w, 0);

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

    if FAILED (p->GetLightParams(m_Pos))

    {

    ShowError();

    return 0;
    }

    // Мы не надеемся на упорядоченность идентификаторов
    // элементов и поэтому заводим массив отображений
    UINT IDs[] =
    {

    IDC_XPOS,
    IDC_YPOS,
    IDC_ZPOS,

    IDC_AMBIENT,

    IDC_DIFFUSE,

    IDC_SPECULAR,

    IDC_AMBMAT,

    IDC_DIFFMAT,

    IDC_SPECMAT,

    IDC_SHINE,

    IDC_EMISSION

    };

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

    for (int i=0;
    Ksizeof (IDs)/sizeof (IDs [0] ) ; i++)
    {

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

    hwnd = GetDlgItem(IDs[i]);

    UINT nID;

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

    int num = GetSliderNum(hwnd, nID);
    //====== Выставляем позицию

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

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

    char s [ 8 ] ;

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

    SetDlgltemText(nID, s);
    }

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

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

    {

    ShowError ();

    return 0;
    }

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

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

    }

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

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

    // Получаем ID по известному описателю окна
    switch (: :GetDlgCtrlI)(hwnd) )
    {
    case IDC_XPOS:

    nID = IDC_XPOS_TEXT;

    return 0; case IDC_YPOS:

    nID = IDC_YPOS_TEXT;

    return 1 ; case IDC_ZPOS:

    nID = IDC_ZPOS_TEXT;

    return 2; case IDC_AMBIENT:

    nID = IDC_AMB_TEXT;

    return 3; case IDC_DIFFUSE:

    nID = IDC_DIFFUSE_TEXT;

    return 4 ;

    case IDC_SPECULAR:

    nID = 1DC_SPECULAR_TEXT;

    return 5; case IDC_AMBMAT:

    nID = IDC_AMBMAT_TEXT;

    return 6; case IDC_DIFFMAT:

    nID = IDC_DIFFMAT_TEXT;

    return 7; case IDC_SPECMAT:

    nID = IDC_SPECMAT_TEXT;

    return 8; case IDC_SHINE:

    nID = IDC_SHINE_TEXT;

    return 9; case IDC_EMISSION:

    nID = IDC_EMISSION_TEXT;

    return 10;
    }

    return 0;
    }

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

    void CPropDlg::ShowError()

    {
    USES_CONVERSION;

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

    CComPtr pError;

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

    CComBSTR sError;

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

    GetErrorlnfo (0, &pError);
    pError->GetDescription(SsError);

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

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

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

  • Дайте команду Debug > Windows > Call Stack (или Alt+7).
  • Внедрите это окно, если необходимо, в блок окон отладчика (внизу экрана).
  • Убедитесь, что вызов произошел из функции DialogРгос одного из базовых классов, точнее шаблонов классов, CDialoglmplBaseT.
  • Этот опыт иллюстрирует тот факт, что все необычно в мире ATL. Этот мир устроен совсем не так, как MFC. Шаблоны классов дают удивительную гибкость всей конструкции, способность приспосабливаться и подстраиваться. Теперь рассмотрим вторую, весьма вероятную, ошибку. Секцию protected в классе CPropDlg следует правильно разместить (странно, не правда ли?). Лучше это сделать так, чтобы сразу за ней шло объявление какой-либо из существующих секций public. Если поместить ее, например, перед макросом

    DECLARE_REGISTRY_RESOURCEID(IDR__PROPDLG)

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

    Сообщение о прокрутке в окне
    Сообщение WM_HSCROLL приходит в окно диалога (читайте: объекту диалогового класса, связанного с окном) всякий раз, как пользователь изменяет положение одного из ползунков, расположенных на лице диалога. Это довольно удобно, так как мы можем в одной функции обработки (onHScroll) отследить изменения, произошедшие в любом из 11 регуляторов. Введите коды обработки этого сообщения, которые сходны с кодами, приведенными в приложении на основе MFC, за исключением СОМ-специфики общения между классами CPropDlg и COpenGL:
    LRESULT CPropDlg::OnHScroll(UINT /*uMsg*/, WPARAM wParam,

    LPARAM iParam, BOOL& /*bHandled*/)
    {
    //====== Информация о событии запакована в wParara

    int nCode = LOWORD(wParam), nPos = HIWORD(wParam), delta, newPos;

    HWND hwnd = (HWND) IParam;

    // Выясняем номер и идентификатор активного ползунка

    UINT nID;

    int num = GetSliderNum(hwnd, nID);

    //====== Выясняем суть события

    switch (nCode)

    {

    case SB_THUMBTRACK:

    case SBJTHUMBPOSITION:

    m_Pos[num] = nPos;

    break;
    //====== Сдвиг до упора влево (клавиша Home)

    case SB_LEFT:

    delta = -100;

    goto New_Pos;
    //====== Сдвиг до упора вправо (клавиша End)

    case SB_RIGHT:

    delta = + 100;

    goto New_Pos;
    case SB_LINELEFT:
    // И т.д.

    delta = -1;

    goto New_Pos;
    case SB_LINERIGHT:

    delta = +1;

    goto New_Pos;
    case SB_PAGELEFT:

    delta = -20;

    goto New_Pos;
    case SB_PAGERIGHT:

    delta = +20;

    goto New_Pos;
    New_Pos:

    newPos = m_Pos[num] + delta;

    m_Pos[num] = newPos<0 ? 0

    : newPos>100 ? 100 : newPos;

    break;

    case SB_ENDSCROLL: default:

    return 0;
    }

    //=== Готовим текстовое выражение позиции ползунка

    char s[8];

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

    SetDlgltemText(nID, (LPCTSTR)s);

    //====== Цикл пробега по всем объектам типа PropDlg

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

    //====== Добываем интеофейсн:

    //====== Добываем интерфейсный указатель

    CComQIPtr p (m_ppUnk[i] ) ;
    //====== Устанавливаем конкретный параметр

    if FAILED (p->SetLightParam (num, m_Pos [num] ) )

    ShowError();
    return 0;
    }
    }

    return 0;

    }

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

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

  • Откройте в окне редактора Studio.Net шаблон окна диалога IDD_PROPDLG.
  • Поставьте фокус в окно выпадающего списка IDC_FILLMODE и переведите фокус окно Properties.
  • Нажмите кнопку Control Events, расположенную на инструментальной панели окна Properties.
  • Найдите строку с идентификатором уведомляющего сообщения CBN_SELCHANGE и в ячейке справа выберите действие , для того чтобы там появилось имя функции обработки OnSelchangeFillmode.
  • Перейдите в окно PropDlg.cpp и введите следующие коды в заготовку функции OnSelchangeFillmode.
  • LRESULT CPropDlg
    ::OnSelchangeFillmode(WORD/*wNotifyCode*/, WORD /*wID*/,
    HWND hWndCtl, BOOL& bHandled)
    {
    //====== Цикл пробега по всем объектам типа PropDlg

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

    CComQIPtr p(m_ppUnk[i]);

    // Выясняем индекс строки, выбранной в окне списка

    DWORD sel = (DWORD)SendMessage(hWndCtl, CB_GETCURSEL,0,0);

    // Преобразуем индекс в режим отображения полигонов

    sel = sel==0 ? GL_POINT

    : sel==l ? GL_LINE : GL_FILL;

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

    if FAILED (p->SetFillMode(sel))
    {

    ShowError();
    return 0;
    }
    }

    bHandled = TRUE;
    return 0;
    }

    Обратите внимание на то, что нам пришлось убирать два комментария, чтобы сделать видимым параметры hWndCtl и bHandled.

    Реакция на нажатия кнопок
    При создании отклика на выбор режима изображения полигонов следует учесть попеременное изменение текста и состояния кнопки. Поставьте курсор на кнопку IDC_QUADS и в окне Properties нажмите кнопку Control Events. Затем найдите строку с идентификатором уведомляющего сообщения BN_CLICKED и в ячейке справа выберите действие . Текст в ячейке должен измениться и стать OnClickedQuads. Введите следующие коды в заготовку функции:

    LRESULT CPropDlg::OnClickedQuads(WORD /*wNotifyCode*/,

    WORD /*wID*/, HWND /*hWndCtl*/, BOOL& bHandled)
    {
    //====== По всем объектам PropDlg

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

    //====== Добываем интерфейсный указатель

    CComQIPtr p(m_ppUnk[i]) ;

    //====== Переключаем режим

    m_bQuad = !m_bQuad;

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

    SetDlgltemText(IDC_QUADS, m_bQuad ? "Quads" : "Strip");
    if FAILED (p->SetQuad(m_bQuad))

    {

    ShowError();
    return 0;

    bHandled = TRUE;
    return 0;
    }

    Аналогичные, но более простые действия следует произвести в реакции на нажатие кнопки Выбор файла. Введите функцию для обработки этого события и вставьте в нее следующий код:

    LRESULT CPropDlg: rOnCl'ickedFilename (WORD /*wNotif yCode*/,

    WORD /*wID*/, HWND /*hWndCtl*/, BOOL& bHandled)
    {

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

    CComQIPtr p (m_ppUnk [i] ) ;
    //====== Вызываем функцию класса COpenGL

    if FAILED (p->ReadData() )
    {

    ShowError () ;
    return 0 ;
    }

    bHandled = TRUE;
    return 0;
    }

    Постройте сервер и проверьте работу страницы свойств. Попробуйте прочесть другой файл, например тот, который был создан приложением, созданным в рамках MFC. Так как мы не изменяли формат данных, записываемых в файл, то все старые файлы должны читаться.

    Управление объектом с помощью мыши
    Алгоритм управления ориентацией объекта с помощью мыши мы разработали ранее. Вы помните, что перемещение курсора мыши при нажатой кнопке должно вращать изображение, причем горизонтальное перемещение вращает его вокруг вертикальной оси Y, а вертикальное — вокруг горизонтальной оси X. Если одновременно с мышью нажата клавиша Ctrl, то объект перемещается (glTranslatef) вдоль осей X и Y. Наконец, с помощью правой кнопки изображение перемещается вдоль оси Z, то есть приближается или отдаляется. Таймер помогает нам в том, что продолжает вращение, если очередной квант перемещения мышью стал выше порога чувствительности. Скорость вращения имеет два пространственных компонента, которые пропорциональны разности двух последовательных во времени координат курсора. Чем быстрее движется курсор при нажатой левой кнопке, тем большая разность координат будет обнаружена в обработчике сообщения WM_MOUSEMOVE. Именно в этой функции оценивается желаемая скорость вращения.

    Описанный алгоритм обеспечивает гибкое и довольно естественное управление ориентацией объекта, но, как вы помните, он имеет недостаток, который проявляется, когда модуль угла поворота вдоль первой из вращаемых (с помощью glRotate) осей, в нашем случае — это ось X, превышает 90 градусов. Вам, читатель, я рекомендовал самостоятельно решить эту проблему и устранить недостаток. Ниже приводится одно из возможных решений. Если вы, читатель, найдете более изящное, буду рад получить его от вас. Для начала следует ввести в состав класса COpenGL функцию нормировки углов вращения, которая, учитывая периодичность процесса, ограничивает их так, чтобы они не выходили из диапазона (-360°, 360°):

    void COpenGL::LimitAngles()
    {
    //====== Нормирование углов поворота так,

    //====== чтобы они были в диапазоне (-360°, +360°)

    while (m_AngleX < -360.f)

    m_AngleX += 360.f;
    while (m_AngleX > 360.f)

    m_AngleX -= 360.f;
    while (m_AngleY < -360.f)

    m_AngleY += 360.f;
    while (m_AngleY > 360.f)

    m_AngleY -= 360.f;
    }

    Затем следует вставить вызовы этой функции в те точки программы, где изменяются значения углов. Кроме того, надо менять знак приращение m_dx, если абсолютная величина угла m_AngleX попадает в диапазон (90°, 270°). Это надо делать при обработке сообщения WM_MOUSEMOVE. Ниже приведена новая версия функции обработки этого сообщения, а также сообщения WM_TIMER, в которое также следует ввести вызов функции нормировки:

    LRESULT COpenGL::OnMouseMove(UINT /*uMsg*/, WPARAM wParam, LPARAM IParam, BOOL& bHandled)

    {
    //====== Если был захват

    if (m_bCaptured)
    {

    //====== Вычисляем желаемую скорость вращения

    short xPos = (short)LOWORD(IParam);

    short yPos = (short)HIWORD(1Param);

    m_dy = float(yPos - m_yPos)/20.f;

    m_dx = float(xPos - m_xPos)/20.f;

    //====== Если одновременно была нажата Ctrl,

    if (wParam & MK_CONTROL)
    {

    //=== Изменяем коэффициенты сдвига изображения

    m_xTrans += m_dx;

    m_yTrans -= m_dy;
    }

    else
    {

    //====== Если была нажата правая кнопка

    if (m_bRightButton)

    //====== Усредняем величину сдвига

    m_zTrans += (m_dx + m_dy)/2.f;
    else
    {

    //====== Иначе, изменяем углы поворота

    //====== Сначала нормируем оба угла

    LiraitAngles();

    //=== Затем вычисляем модуль одного из них

    double a = fabs(m_AngleX);

    // и изменяем знак приращения(если надо)

    if (90. < а && а < 270.) m_dx = -m_dx;

    m_AngleX += m_dy;

    m_AngleY += m_dx;
    }
    }

    // В любом случае запоминаем новое положение мыши

    m_xPos = xPos;

    m_yPos = yPos;

    FireViewChange();
    }

    bHandled = TRUE; return 0;
    }

    LRESULT COpenGL: :OnTimer (UINT /*uMsg*/, WPARAM

    /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)
    {
    //====== Нормировка углов поворота

    LimitAngles () ;

    //====== Увеличиваем эти углы

    m_AngleX += m_dy; m_AngleY += m_dx;

    //====== Просим перерисовать окно

    FireViewChange();
    bHandled = TRUE;
    return 0;

    }

    Ниже приведены функции обработки других сообщений мыши. Они сходны с теми, которые мы разработали для MFC-приложения, за исключением прототипов и возвращаемых значений. Начнем с обработки нажатия левой кнопки. Оно, очевидно, должно всегда останавливать таймер, запоминать факт нажатия кнопки и текущие координаты курсора мыши:

    LRESULT COpenGL::OnLButtonDown(UINT /*uMsg*/, WPARAM

    /*wParam*/, LPARAM IParam, BOOL& bHandled)
    {

    //====== Останавливаем таймер

    KillTimer(1);

    //====== Обнуляем кванты перемещения

    m_dx = O.f;

    m_dy = 0.f;

    //====== Захватываем сообщения мыши,

    //====== направляя их в свое окно

    SetCapture();

    //====== Запоминаем факт захвата

    m_bCaptured = true;
    //====== Запоминаем координаты курсора

    m_xPos = (short)LOWORD(IParam);
    m_yPos = (short)HIWORD(IParam);
    bHandled = TRUE; return 0;
    }

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

    LRESULT COpenGL::OnLButtonUp(UINT /*uMsg*/, WPARAM

    /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)
    {
    //====== Если был захват,

    if (m_bCaptured)
    {

    //=== то анализируем желаемый квант перемещения
    //=== на превышение порога чувствительности
    if (fabs(m_dx) > 0.5f || fabs(m_dy) > 0.5f)

    //====== Включаем режим постоянного вращения

    SetTimer(1,33) ;
    else

    //====== Выключаем режим постоянного вращения

    KillTimer(1);

    //====== Снимаем флаг захвата мыши

    m_bCaptured = false;

    //====== Отпускаем сообщения мыши

    ReleaseCapture();
    }

    bHandled = TRUE;
    return 0;
    }

    При нажатии на правую кнопку выполняются те же действия, что и при нажатии на левую, но дополнительно запоминается факт нажатия правой кнопки, с тем чтобы можно было правильно интерпретировать последующие сообщения о перемещении указателя мыши и вместо вращения изображения производить его сдвиг вдоль оси Z. Отметьте тот факт, что мы должны убрать символы комментариев вокруг параметров:

    LRESULT COpenGL::OnRButtonDown(UINT uMsg, WPARAM wParam,

    LPARAM IParam, BOOL& bHandled)
    {

    //====== Запоминаем факт нажатия правой кнопки

    m_bRightButton = true;

    //====== Воспроизводим реакцию на левую кнопку

    OnLButtonDown(uMsg, wParam, IParam, bHandled);
    return 0;
    }

    Отпускание правой кнопки просто отмечает факт прекращения перемещения вдоль оси Z и отпускает сообщения мыши (ReleaseCapture), для того чтобы они могли правильно обрабатываться другими окнами, в том числе и нашим окном-рамкой. Если этого не сделать, то будет невозможно использоваться меню:

    LRESULT COpenGL::OnRButtonUp(UINT /*uMsg*/, WPARAM

    /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)
    {

    m_bRightButton = false;

    m_bCaptured = false;

    ReleaseCapture();

    bHandled = TRUE;

    return 0;
    }

    Запустите и проверьте управляемость объекта. Введите коррективы чувствительности мыши. В заключение отметим, что при выборе параметров заготовки ATL мы могли на вкладке Miscellaneous (Разное) поднять не только флажок Insertable, но и windowed Only. Это действие сэкономило бы те усилия, которые были потрачены на поиск неполадок, вызванных отсутствием флага m bWindowOnly.

    Cамоучитель по Visual Studio.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). Элементы в таком контейнере не упорядочены, хотя их можно добывать последовательно. Если вы вставите или удалите элемент, то последовательность оставшихся элементов может измениться, то есть она не гарантируется. Преимуществом рассматриваемого типа контейнеров является то, что в среднем они значительно быстрее сортированных ассоциативных контейнеров. Удачно подобранная функция хеширования позволяет выполнять вставки, удаления и поиск за постоянное, не зависящее от п, время. Кроме того, она обеспечивает равномерное распределение хешированных значений и минимизирует количество коллизий.

    Примечание
    Примечание

    Поясним кратко суть хеширования. Она состоит в том, что каждому ключу (key) ставится в соответствие значение (value). Например, ключом могло бы быть имя абонента — строка символов, а значением — номер его телефона. Поиск значения по ключу осуществляется с помощью хеш-таблицы (hash table), которая ассоциирует ключевой объект с объектом типа значение. Эффективность работы зависит от алгоритма хеширования, который преобразует ключ в число из какого-то диапазона. Это число еще не value, а скорее индекс для выбора значения (value). При этом возможна ситуация коллизии (collision), когда два разных ключа будут преобразованы в одно и то же число. В таких случаях производится обработка коллизии по специальному алгоритму. Обычно используются списки для хранения ключей, случайно попавших в коллизию, или, как говорят, в одно ведро (bucket). Списки — это потеря эффективности поиска, но хорошие алгоритмы хеширования гарантируют очень низкую вероятность коллизий.
    Если ключи не уникальны, то можно выбрать не hаsh_mар-контейнер, а контейнер типа hash_multimap. Если нужно просто хранить множество каких-то объектов, например строк текста, не ассоциируя их с другими объектами, то стоит подумать о контейнере типа hash_set. Ну а в случае, если среди этих объектов могут попасться одинаковые, то выбором может стать контейнер типа hash_multiset.

    Хешируемые типы контейнеров все-таки упорядочивают контролируемую ими последовательность, вызывая встроенный в него объект hash Traits класса value_compare. Вы имеете доступ к этому объекту с помощью метода key_comp. В общем случае два элемента признаются либо одинаковыми, либо какой-либо из них признается меньшим. Но реальная упорядоченность элементов зависит от функции хеширования, функции, задающей отношение порядка и текущего размера хэш-таблицы, поэтому в общем случае невозможно предсказать порядок следования элементов. Важной характеристикой ассоциативных контейнеров является то, что вставка элементов не портит итераторы, а удаление делает недействительными только те итераторы, которые указывают на удаляемый элемент.

    с приоритетами тоже является адаптером,

    Контейнеры типа priority_queue
    Очередь с приоритетами тоже является адаптером, который позволяет вставку элементов, инспекцию и удаление верхнего (top) элемента. Она не допускает итераций прохода по своим элементам. Ее характерным отличием является то, что верхний элемент является самым большим в том смысле, в котором в шаблоне используется функциональный объект (Compare — сравнение объектов). Для разнообразия приведем объявление шаблона:
    template
    <

    class Type,

    class Container=vector,

    class Compare=less
    >

    Отсюда видно, что по умолчанию очередь с приоритетами основана на контейнере типа vector и для сравнения приоритетов она использует предикат lesso. Для объектов класса Man — это внешняя friend-функция operator< (), которая упорядочивает последовательность по возрасту. Но очередь с приоритетами должна расставить элементы по убыванию приоритетов. Проверим это утверждение с помощью следующего фрагмента:

    void main () {

    //===== Priority queue (by age)

    priority_queue men;

    men.push (zoran);

    //== Для проверки поведения вставляем объект повторно

    men.push (zoran);

    men.push (joy);

    men.push (mela); men.push (win);

    cout«"priority_queue size: "«men. size () «endl;

    int i=0;

    while ('men.empty())

    {

    cout « "\n"« ++i«". "«men.top();

    men.pop();
    }
    }

    Выходом этой программы будет такой текст:

    priority_queue size: 5

    1. Winton Kelly, Age: 50

    2. Zoran Todorovitch, Age: 27

    3. Zoran Todorovitch, Age: 27

    4. Joy Amore, Age: 18

    5. Melissa Robinson, Age: 9
    Как видно, объекты выстроены по убыванию возраста. Очереди и стеки допускают повторение элементов.

    Использование STL

    Использование STL

    В подобных ситуациях владение стандартными динамическими структурами данных и алгоритмами может сэкономить массу усилий, так как их разработчики уже выполнили большую часть неблагодарной черновой работы, тщательно отладили динамику жизни структур данных и все ветви алгоритмов. Кроме того, они провели анализ эффективности алгоритмов и привели их оценки. Сравним для примера две реализации алгоритма сортировки. Все знают, что рекурсивный алгоритм быстрой сортировки Quicksort, —изобретенный С. A. R. Ноаге в 1960 году, считается одним из самых эффективных в смысле количества необходимых операций для выполнения работы. Так, для сортировки массива в п элементов этому алгоритму понадобится всего лишь O(n Iog2 n) операций.
    В библиотеке, подключаемой файлом заголовков stdlib.h, есть функция qsort, которая использует алгоритм Quicksort для сортировки массива элементов произвольного типа. Кроме сортируемого массива в функцию qsort необходимо передать адрес функции, которая сравнивает два элемента между собой. Алгоритм использует это сравнение для упорядочивания массива. Следующая программа демонстрирует, как можно воспользоваться функцией qsort для сортировки массива целых, вводимого пользователем. Для ее отладки я воспользовался проектом Console консольного типа, процедура создания которого была описана ранее. Из-за ошибок, связанных с использованием бета-версии Studio.Net, мне пришлось изменить конфигурацию проекта с Debug на Release. Это можно сделать, дав команду Build > Configuration Manager и выбрав Release в окне Active Solution Configuration:

    #include
    #include
    using namespace std;

    //=== Внешняя функция сравнения переменных типа int

    inline int crop (const void *a, const void *b)

    {

    int i = *(int *)a, j = *(int *)b;

    return (i < j) ? -1 : (i > j) ? 1 : 0;
    }

    void main()

    {

    int array [1024],
    // Сортируемый массив n - 0;
    // Счетчик элементов

    cout «"Enter some integers (Press Ctrl+z to stop)\n";

    //=== Вводим по принципу "пока не надоест". Для выхода

    //=== из цикла надо ввести EOF (то есть Ctrl+z, Enter)

    while (cin » array[n++])

    //==== Шаг назад, так как мы сосчитали EOF n—;

    qsort (array, n, sizeof(int), cmp) ;

    for (int i = 0; i < n; i++)
    cout « array[i] « endl;
    cout « endl;
    }

    Теперь сравним этот фрагмент с тем, который использует стандартный контейнер vector и алгоритм sort из библиотеки STL (Standard Template Library):

    #include

    #include

    #include

    using namespace std;

    void main ()
    {

    vector v; // Сортируемый контейнер

    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 раз. Она зависит от размера массива и степени его упорядоченности. Приведем одну из причин такого результата.

  • Универсальность первого алгоритма реализуется на этапе выполнения за счет вызова generic-функции стр () и преобразования типов указателей.
  • Универсальность второго подхода реализуется за счет настройки шаблона на конкретный тип переменных, что происходит на этапе компиляции.
  • Важно помнить, что рекурсия сама по себе стоит дорого, поэтому важны детали реализации конкретного алгоритма. Над деталями реализации алгоритмов библиотеки STL потрудились специалисты, и результатом их труда является достаточно высокая эффективность, которую отмечают многие разработчики. К сожалению, возможности STL очень скудно описаны в MSDN, хотя в мире существуют книги, где библиотека и технология ее использования для решения конкретных задач описаны достаточно подробно. Среди доступных нам книг на русском языке, конечно, следует отметить последнюю книгу Б. Страуструпа «Язык программирования C++», 3-е изд. — СПб: «Невский Диалект», 1999. Но она описывает библиотеку концептуально. В ней почти нет текстов программ, готовых к употреблению в среде Visual Studio. Поэтому мне захотелось дать быстрый путь к овладению некоторыми возможностями библиотеки тем читателям, которые обладают хорошим алгоритмическим мышлением, имеют некоторый опыт работы с динамическими структурами данных, но не знакомы с особенностями структуры и использования STL. Ниже будут приведены примеры практического использования контейнеров и алгоритмов STL, но не будет подробного описания заложенных в них принципов.

    Из жизни студентов

    Из жизни студентов
  • Использование STL
  • Вектор объектов класса
  • Предикаты и функциональные объекты
  • Связыватели и адаптеры
  • Последовательности
  • Контейнеры
  • Работа с потоками
  • Полезные константы
  • Как показывает практика, студенты по-разному относятся к тому факту, что доля курсовых проектов, которые необходимо выполнять в виде компьютерных приложений, непрерывно растет. Некоторые их очень любят, так как подобные проекты позволяют продемонстрировать неординарность мышления, изобретательность и свой собственный «неподражаемый» стиль программирования, другие ненавидят, так как работающее приложение невозможно создать без тщательной проработки почти всех деталей, в том числе и тех, которые кажутся мелкими и незначительными. Сначала компилятор языка, а затем и операционная система хладнокровно бракуют малейшую неточность, непоследовательность, недоговоренность и пренебрежение деталями. В устном докладе и даже в письменном отчете можно скрыть или завуалировать перечисленные дефекты, но компьютерный проект обнажит их, продемонстрирует со всей очевидностью, а зачастую и усилит.

    Контейнер типа set

    Контейнер типа set

    Множество (set) является ассоциативным контейнером, который хранит объекты типа key. В этом случае говорят о типе Simple Associative Container, имея в виду, что как value, так и key имеют один тип key. Говорят также о Unique Associative Container, имея в виду, что в контейнере типа set не может быть одинаковых элементов. Рассмотрим работу контейнера на примерах. Не забудьте вставить директиву #include :

    void main ()
    {

    //======== Создаем множество целых

    set s;

    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 ss;
    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 A(a, a + N);
    set B(b, b + N);

    //======== Создаем пустое множество

    set C;

    //======== Выходной итератор привязываем к cout

    cout « "Set A: {";
    copy (A.begin (), A.end.(),

    ostream_iterator(cout, "; "));
    cout « ' } ' ;

    cout « "\n\nSet B:

    copy (B.begin (), B.end(), .. ostream_iterator(cout, ", "));

    //======= Создаем и выводим объединение двух множеств

    cout « "\n\nUnion A U В: ";

    set_union (A.begin () , A.end(), B.begin(), B.end(),

    ostream_iterator(cout, ", "),

    LessStr () )';

    //======= Создаем и выводим пересечение двух множеств

    cout « "\n\nlntersection А & В: ";

    set_intersection (A.begin () , A.end(), B.beginO, B.end(), ostream_iterator(cout, " "), LessStrO);

    //===== Создаем разность двух множеств

    //===== Используем 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(cout, " "));
    С. 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, " "));
    cout « "\n\n";

    //====== Выводим разделитель

    vector line(50, ' = ');
    ostream_iterator os(cout, "") ;

    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, " "), NoCase);

    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 result = FindRoot();
    if (result.first)

    print(result.second);
    else

    report_error() ;

    Ассоциативные контейнеры используют тип пар _Pairib. Он определяет пару:

    pairbool>

    Первый элемент каждой такой пары является итератором соответствующего типа, а второй — результатом какого-либо действия. Например, метод insert возвращает пару типа _Pairib, анализируя которую вы можете узнать результат вставки (успех или неудача). Рассмотрим пример:

    void main ()
    {
    //========== Массив объектов класса Man

    Man ar[] =
    {

    joy,duke, win, joy,charlie

    );

    uint size = sizeof(ar)/sizeof(Man);

    //========== Создаем множество объектов класса Man

    set s (ar, ar+size); pr(s, "Set of Man");

    //========== Ищем объект и удаляем его

    set::iterator p = s.find(joy);

    if (p != s.end() )
    {

    s.erase(p);

    cout « "\n\n"« joy «" found and erased";
    }
    pr(s,"After erasure");

    //========== Объявляем пару

    set::_Pairib pib;

    //========== Пробуем вставить объект

    pib = s.insert(joy);

    //========== Анализируем результат вставки

    cout « "\n\nlnserting: " « *pib.first « "\nResult is: " « pib.second;

    //========== Пробуем вставить повторно

    pib = s.insert(joy);

    cout « "\n\nlnserting: " « *pib.first « "\nResult is: " « pib.second;

    //========== Сравниваем ключи

    cout « "\n\ns.key_comp() (zoran,count) returned "

    « s.key_comp()(zoran,ar[0]);
    cout « "\n\ns.key_comp()(count,zoran) returned "

    « s.key_comp()(ar[0],zoran);
    cout <<"\n\n";
    }

    Приведем результат работы этой программы:

    Set of Man # Sequence:

    1. Joy Amore, Age: 18

    2. Winton Kelly, Age: 50

    3. Charlie Parker, Age: 60

    4. Duke Ellington, Age: 90

    Joy Amore, Age: 18 found and erased After erasure # Sequence:

    1. Winton Kelly, Age: 50

    2. Charlie Parker, Age: 60

    3. Duke Ellington, Age: 90

    Inserting: Joy Amore, Age: 18

    Result is: 1

    Inserting: Joy Amore, Age: 18

    Result is: 0

    s.key_comp()(zoran,count) returned 0 s.key_comp()(count,zoran) returned 1

    Контейнеры библиотеки STL

    Контейнеры библиотеки STL

    Теперь, когда вы вспомнили, что такое шаблоны функций и шаблоны классов, мы можем исследовать возможности стандартной библиотеки шаблонов STL. В июле 1994 года специальный комитет Международной организации по принятию стандартов (ANSI/ISO C++) проголосовал за то, чтобы принять STL в качестве части стандарта языка C++. Предложение было основано на исследовании обобщенного (generic) программирования и концепции библиотеки (generic software library), которое проводили Alex Stepanov, Meng Lee и David Musser. Главной целью при разработке библиотеки было достижение общности (generality) подхода к различным структурам данных и алгоритмам их обработки без ущерба эффективности кода.
    В STL определены два типа контейнеров — последовательности (sequence containers) и ассоциативные контейнеры. Все контейнеры предназначены для хранения данных любого типа. Последовательности предполагают последовательный доступ к своим элементам, а ассоциативные контейнеры работают по принципу ассоциации ключа (key) с его значением (value). Можно считать, что ассоциативные контейнеры хранят пары произвольных элементов и производят поиск по ключу, используя технику hash-таблиц. В STL существует три типа последовательных контейнеров: vector, deque И list.

    Контейнеры типа hash_multimap

    Контейнеры типа hash_multimap
    Хешированный ассоциативный контейнер типа hash_multimap основан на встроенной реализации хэш-таблиц. Вы помните, что преимуществом такого типа контейнеров является быстродействие, которое в среднем значительно выше, чем у сортированных ассоциативных контейнеров. Упорядоченность элементов в таком контейнере не гарантируется, но вы можете по определенной системе добывать их С ПОМОЩЬЮ метода hash_multimap: :equal_range.

    Предположим, что ваша база данных содержит сведения о сотрудниках — объектах класса Man, многих отделов какой-то организации. В примере мы возьмем только два отдела (100 и 115). Так как мы хотим быстро получать информацию о сотрудниках, то выбираем в качестве структуры для хранения данных в памяти хешированный ассоциативный контейнер. Очевидно, что если в качестве ключевого поля для него выбрать номер отдела, то поле не будет уникальным. Этот факт окончательно определяет выбор типа контейнера— hash_multimap.

    Вы также, вероятно, помните, что все контейнеры типа тар — это Pair Associative контейнеры, так как они хранят пары типа pair. В нашем случае этой парой является pair, где первый элемент задает номер отдела, а второй сотрудника этого отдела. Для удобства пользования контейнером введем новые типы данных:

    //======= ManPair - это тип используемых пар

    typedef pair Man> ManPair;

    //======= ManMap - это тип контейнера

    typedef hash_multimap Man> ManMap;

    //======= ManMapIt — это тип итератора

    typedef ManMap::const_iterator ManMapIt;

    Отметьте, что мы выбрали самый простой способ определения контейнера. Более точным описанием, которое намекает вам на возможность усложнения структуры, будет:

    typedef hash_multimap Man,

    hash_compare > > ManMap;

    Отсюда ясно, что можно изменить предикат, по которому производится сравнение элементов контейнера. Для выбора всех сотрудников определенного отдела мы собираемся использовать метод:

    equal_range(int /*Номер отдела*/);

    который возвращает пару итераторов. Первый итератор пары указывает на начало диапазона внутри контейнера из сотрудников указанного отдела, а второй — на конец этого диапазона. Теперь пора в бой. Надо писать код, реализующий работу контейнера.

    void main( )
    {

    typedef pair Man> ManPair;

    typedef hash_multimap Man> ManMap;

    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 pp = h.equal_range(100);

    //====== Вновь пользуемся парой

    cout « "\n\nEmployees of 100 department\n\n";
    for (p = pp.first; p != pp.second; ++p)
    cout « "\n" « p->first

    «"." « p->second; cout « "\n\n";
    }

    He лишнее напомнить, что приведенный код надо дополнить объявлениями объектов класса Man и вставкой директивы #include . Директивы должны копиться. Я надеюсь, что с этой задачей вы справитесь самостоятельно. Объявления людей мы приводили где-то в начале урока. Программа должна произвести такой вывод:

    Contents of Hash Multimap

    115. Liza Dale, Age: 17

    115. Joy Amore, Age: 18

    115. Joe Doe, Age: 30

    100. Winton Kelly, Age: 50

    100. Charlie Parker, Age: 60

    100. Mary Poppins, Age: 36

    Employees of 100 department

    100. Winton Kelly, Age: 50

    100. Charlie Parker, Age: 60

    100. Mary Poppins, Age: 36

    Контейнеры типа map

    Контейнеры типа map
    Отображение (map) является сортируемым ассоциативным контейнером, который ассоциирует объекты типа key с объектами типа value. Map — это Pair Associative Container, так как он хранит пары типа pair. Говорят также о Unique Associative Container, имея в виду, что в контейнере типа тар не может быть одинаковых элементов. Отображения являются одними из самых мощных и удобных типов контейнеров. Рассмотрим их работу на примерах. Не забудьте вставить директиву #include :

    void main ()
    {

    //========= Создаем отображение строки в целое

    mapint>
    m;

    mapint>:
    :_Pairib pib;

    mapint>::iterator it;

    //========= Создаем новый тип для удобства

    typedef pairint> MyPair; MyPair p("Monday", 1); m.insert(p);

    //========= Изменяем компоненты пары

    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 pairs :\n";

    for (it = m.begin ();
    if != m.end(); it++)

    cout « "\n(" « it->first«","<second«") ";
    cout«"\n\n";
    }

    Результат работы этого фрагмента выглядит так:

    Inserting: Tusday, 2 Result is: 1

    Inserting: Tusday, 2 Result is: 0

    pairs:

    (Friday, 5) (Monday, 1) (Saturday, 6) (Thirsday, 4) (Tusday, 2) (Wednesday, 3)

    Как видите, пары отсортированы в лексикографическом порядке. Если потребуется восстановить естественный порядок, то это можно сделать, поменяв порядок следования аргументов при объявлении шаблона на

    mapstring> m;

    Такую замену придется сделать и для всех других, связанных с шаблоном типов данных. Отметьте также, что при работе с отображениями недостаточно разадресо-вать итератор (*it), чтобы получить объект им указываемый. Теперь вы должны писать (*it) .first или it->first, чтобы получить какой-то объект. Характерно, что эти выражения могут стоять как в левой, так и в правой части операции присвоения, то есть вы можете записать:

    it->first = "Sunday";
    int n = it->second;

    Контейнеры типа queue

    Контейнеры типа queue
    Очередь — это тоже,адаптер, который предоставляет ограниченное подмножество функциональности контейнера. Говорят, что очередь — это структура данных с дисциплиной доступа "first in first out" (FIFO). Элементы, вставляемые в конец очереди, могут быть выбраны спереди. Это означает, что метод queue:: front () возвращает самый «старый» элемент, то есть тот, который был вставлен в очередь least recently — первым из тех, что еще живы. Очередь, так же как и стек, не допускает итераций прохода по своим элементам. По умолчанию она основана на контейнере типа deque. Сравнение стека и очереди приведены в следующем фрагменте (Подключите ):

    void main ()
    {

    //========== Массив объектов класса Man

    Man ar[] =

    {

    joy, mаrу, win

    };

    uint size = sizeof(ar)/sizeof(Man);

    //========== Создаем с.тек объектов класса Man

    stack s;

    for (uint i=0; i
    cout « "Stack of Man:\n\n";

    while (s.size ())

    {

    cout « s.top() « "; ";

    s.pop ();
    }

    //========== Создаем очередь объектов класса Man

    queue q;

    for (i=0; Ksize; i++) q.push(ar[i]);

    cout « "\n\nQueue of Man:\n\n";

    while (q.size ())

    {

    cout « q.front() « "; ";

    q.pop(); }

    cout«"\n\n";
    }

    Поиск с помощью предиката

    Поиск с помощью предиката

    Поиск первого объекта, который удовлетворяет условию, заданному предикатом, осуществляется с помощью шаблона функции f ind_if. В качестве третьего, параметра она требует задать имя функции-предиката. Введите в состав класса объявление такой функции:

    //========= Предикат принадлежности к teenager
    friend bool Teen (Man& m);
    Тело этой функции определите глобально, то есть вне класса:

    //========= Предикат принадлежности к teenager

    bool Teen(Man& m)
    {

    return 13 < m.m_Age && m.m_Age < 19;
    }

    Теперь покажем, как искать в контейнере первый элемент, удовлетворяющий предикату, а также все элементы, удовлетворяющие этому условию. Ниже нам понадобятся несколько объектов класса Man, поэтому мы ввели их объявление в начало функции main. Далее везде мы будем считать, что эти объекты присутствуют в функции main, но не будем приводить их заново:

    void main ()

    {

    //======== Набор объектов класса Man

    Man joe("Joe Doe",30),

    joy ("Joy Amore", 18) ,
    Mаrу("Mary Poppins",36),
    duke("Duke Ellington",90),
    liza("Liza Dale", 17),
    simon("Simon Paul",15),
    zoran("Zoran Todorovitch",27) ,
    Charlie("Charlie Parker",60),
    win("Winton Kelly",50),
    mela("Melissa Robinson",9);

    vector men;
    men.push_back (zoran);
    men.push_back (liza);
    men.push_back (simon);
    men.push_back (mela);

    // Поиск первого объекта, удовлетворяющего предикату
    vector::iterator p =

    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::reverse_iterator
    r = men.rbegin();

    r != men.rendO; r++) cout«*r«";

    //======== Заполняем вектор целых

    vector v;
    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 vl(v.size()), v2 (v.size());

    //======== Иллюстрируем алгоритм заполнения вектора

    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. Сложить два контейнера можно и другими способами.

    Если вы подключите файл заголовков , то сможете осуществлять логические проверки с помощью функции assert. Она проверяет свой аргумент и, если он равен false, выводит на экран сообщение вида:

    Assertion failed: s.topQ == joy,

    file C:\My ProjectsXStack.cpp, line 29

    abnormal program termination

    Затем прекращает процесс вызовом функции abort. Если результатом выражения (аргумента функции assert) будет true, то выполнение продолжается.

    Полезные константы

    Полезные константы

    STL имеет много полезных констант. Проверьте свои знания основ информатики. Знаете ли вы смысл констант, приведенных ниже? Для их использования вам потребуется подключить такие файлы заголовков:

    #include

    #include
    #finclude
    #finclude

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

    //===== Сначала простые, которые знают все
    cout « "\n Is a char signed? "

    « numeric_limits::is_signed;
    cout « "\n The minimum value for char is: "

    « (int)numeric_limits::min();
    cout « "\n The maximum value for char is: "

    « (int)numeric_limits::max();
    cout « "\n The minimum value for int is: "

    « numeric_limits::min();
    cout « "\n The maximum value for int is: "

    « numeric_limits::max();
    cout « "\n Is a integer an integer? "

    « numeric_limits::is_integer;
    cout « "\n Is a float an integer? "

    « numeric_limits::is_integer;

    cout « "\n Is a integer exact? "

    « numeric_limits::is_exact;

    cout « "\n Is a float exact? "

    « numeric_limits::is_exact;

    //===== Теперь более сложные

    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::digitslO;
    cout « "\n The radix for float is: "

    « numeric_limits::radix;
    cout « "\n The epsilon for float is: "

    « numeric_limits::epsilon() ;
    cout « "\n The round error for float is: "

    « numeric_limits::round_error();
    cout « "\n The minimum exponent for float is: "

    « numeric_limits::min_exponent;
    cout « "\n The minimum exponent in base 10: "

    « numeric_limits::min_exponentlO;
    cout « "\n The maximum exponent is: "

    « numeric_limits::max_exponent;
    cout « "\n The maximum exponent in base 10: "

    « numeric_limits::max_exponentlO;
    cout « "\n Can float represent positive infinity? "

    « numeric_limits::has_infinity;
    cout « "\n Can double represent positive infinity? "

    « numeric_limits::has_infinity;
    cout « "\n Can int represent positive infinity? "

    « numeric_limits::has_infinity;
    cout « "\n Can float represent a NaN? "

    « numeric_limits::has_quiet_NaN;
    cout « "\n Can float represent a signaling NaN? "

    « numeric_limits::has_signaling_NaN;

    //===== Теперь еще более сложные

    cout « "\n Does float allow denormalized values? "

    « numeric_limits::has_denorm;
    cout « "\n Does float detect denormalization loss? "

    « numeric_limits::has_denorm_loss;
    cout « "\n Representation of positive infinity for"

    " float: "« numeric_limits::infinity();
    cout « "\n Representation of quiet NaN for float: "

    « numeric_limits::quiet_NaN();
    cout « "\n Minimum denormalized number for float: "

    « numeric_limits::denorm_min();
    cout « "\n Minimum positive denormalized value for"

    " float " « numeric_limits::denorm_min();
    cout « "\n Does float adhere to IEC 559 standard? "

    « numeric_limits::is_iec559; cout « "\n Is float bounded? "

    « numeric_limits::is_bounded;
    cout « "\n Is float modulo? "

    « numeric_limits::is_modulo;

    cout « "\n is int modulo? "

    « numeric_limits::is_modulo;
    cout « "\n Is trapping implemented for float? "

    « numeric_limits::traps;
    cout « "\n Is tinyness detected before rounding? "

    « numeric_limits::tinyness_before;
    cout « "\n What is the rounding style for float? "

    « (int)numeric_limits::round_style;
    cout « "\n What is the rounding style for int? "

    « (int)numeric_limits::round_style;
    //===== Теперь из другой оперы
    cout « "\n Floating digits " « FLT_DIG;
    cout « "\n Smallest such that 1.0+DBL_EPSILON !=1.0: "

    « DBL_EPSILON;

    cout « "\n LDBL_MIN_EXP: " « LDBL_MIN_EXP;
    cout « "\n LDBL_EPSILON: " « LDBL_EPSILON;
    cout « "\n Exponent radix: " « _DBL_RADIX;

    Незнание констант типа DBL_EPSILON или DBL_MANT_DIG довольно сильно ограничивает квалификацию программиста, поэтому советую внимательно исследовать вывод, производимый данным фрагментом, и, возможно, обратиться к специальным изданиям по архитектуре компьютера или учебникам с целью ликвидировать пробелы в знаниях в этой области.

    Последовательности типа deque

    Последовательности типа deque

    Контейнер типа deque (очередь с двумя концами) похож на vector в том смысле, что допускает выбор элемента по индексу и делает это быстро. Отличие состоит в том, что он умеет эффективно вставлять новые элементы как в конец, так и в начало последовательности. Deque не имеет некоторых методов, которые имеет vector, например capacity и reserve. Вместо этого он имеет методы, которых нет у вектора, например push_f ront, pop_back и pop_f ront. Далее мы будем исследовать возможности различных контейнеров, и каждый новый контейнер требует подключения своего файла заголовков. В данный момент не забудьте вставить директиву препроцессора tinclude :
    void main ()
    {
    deque d;
    d.push_back(0.5) ;
    d.push_back(l.);
    d.push_front(-1.);

    pr(d,"double Deque");

    //======== Ссылки на два крайних элемента

    deque::reference
    rf = d.front(),
    rb = d.back();

    //======== Присвоение с помощью ссылок

    rf = 100.;
    rb = 100.;
    pr(d,"After using reference");

    //======== Поиск с помощью связывателя

    deque::iterator p = find_if(d.begin(), d.end(),

    bind2nd(less(),100.));

    //======== Вставка в позицию перед позицией,

    //======== на которую указывает итератор

    d.insert(p,-1.);

    pr(d,"After find_if and insert");
    //======== Второй контейнер

    deque dd(2,-100.);

    //======== Вставка диапазона значений

    d.insert (d.begin ()+1, dd.begin(), dd.end());

    pr(d,"After inserting another deque");
    cout«"\n\n";
    }

    Следующий фрагмент демонстрирует, как можно копировать контейнеры (сору) и обменивать данные между ними (swap). Шаблон функций find позволяет найти объект в любой последовательности. Он откажется работать, если в классе объектов не определена операция operator== (). Отметьте также, что после вставки или удаления элемента в контейнер типа deque все итераторы становятся непригодными к использованию (invalid), так как произошло перераспределение памяти. Однако удаление с помощью pop_back или pop_f ront портит только те итераторы, которые показывали на удаленный элемент, остальные можно использовать. При использовании фрагмент надо дополнить объявлениями объектов класса Man:

    void main ()
    {

    deque men;

    men.push_front (Man("Jimmy Young",16));

    men.push_front (simon);

    men.pushjoack (joy);

    pr(men,"Man Deque");

    //======== Поиск точного совпадения

    deque::iterator p =

    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(3,joy); men.resize(d.size ());

    //======== Копируем d в men

    copy(d.begin(), d.end(), men.begin()); pr(men,"After resize and copy");

    //======== Изменяем контейнер

    d.assign(3,win);

    //======== Обмениваем данные

    d.swap(men);

    pr(men,"After swap with another deque"); cout«"\n\n";
    }

    Последовательности типа list

    Последовательности типа list

    Контейнеры типа list представляют собой двусвязные списки, то есть упорядоченные последовательности, допускающие проходы как вперед, так и назад. Операции вставки и удаления одинаково эффективны в любое место списка. Однако операции поиска и выбора элементов линейны относительно размера контейнера. Выбор по индексу вовсе невозможен. Важным свойством списка является то, что операции вставки не портят итераторы, связанные с ним, а удаление делает недействительным только тот итератор, который указывал на удаленный элемент.
    В шаблоне класса list определены методы merge, reverse, unique, remove и remove_if, которые оптимизированы для списков. Не путайте их с одноименными шаблонами функций, которые определены в алгоритмах. В примере, который приведен ниже, обратите внимание на операции слияния как списков, так и контейнеров различной природы. При исследовании списков не забудьте вставить директиву #include , а также приведенный выше набор объектов класса Man:

    void main 0
    {

    list men;

    men.push_front(zorah);

    men.push_back(mela);

    men.push_back(joy);

    pr(men,"Man List");

    //======== Поиск объекта

    list::iterator p = find (men.begin(),men.end(),mela);

    //======== Вставка перед элементом

    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 li(3,simon);

    //======== Сливаем его с первым

    . men.merge (li, less'() );

    pr(men,"After merging with simons list");

    //==== После слияния второй список полностью исчез

    cout « "\n\tAfter merging simons li.size: "

    « li.size() « endl;
    men.remove(s imon);

    //======== Создаем очередь

    deque d(men.size());

    //======== Копируем в нее список

    copy(men.begin(), men.end(), d.begin());
    pr(d,"Deque copied from list");

    //======== Создаем вектор

    vector v (men. size () + d.sizeO);

    //==== Соединяем список и очередь и помещаем в вектор merge(men.begin(),men.end(),d.begin(),d.end(), v.begin() ) ;

    pr(v,"Vector after merging list and deque");
    pr(d,"Deque after merging with list");
    cout«"\n\n";
    }

    После слияния (merge) двух списков (men и li) размер второго списка стал нулевым, так как он полностью влился в первый. При слиянии методом list: emerge элементы не копируются, а вливаются в список-мишень операции. При слиянии с помощью алгоритма merge контейнеры операнды остаются невредимыми, так как они копируются в контейнер-мишень. Если операнды операции слияния упорядочены, то при слиянии методом list::merge упорядоченность не нарушается, чего не наблюдается при слиянии шаблоном функции merge. Приведем для ясности результат работы рассматриваемой программы:

    Man List # Sequence:

    1. Zoran Todorovitch, Age: 27

    2. Melissa Robinson, Age: 9

    3. Joy Amore, Age: 18

    After inserting 3 joes # Sequence:

    1. Zoran Todorovitch, Age: 27

    2. Joe Doe, Age: 30

    3. Joe Doe, Age: 30

    4. Joe Doe, Age: 30

    5. Melissa Robinson, Age: 9 6. Joy Amore, Age: 18

    After removing all joes and sort # Sequence:

    1. Melissa Robinson, Age: 9

    2. Joy Amore, Age: 18

    3. Zoran Todorovitch, Age: 27

    After merging with simons list # Sequence: 1. Melissa Robinson, Age: 9

    2. Simon Paul, Age: 15

    3. Simon Paul, Age: 15

    4. Simon Paul, Age: 15

    5. Joy Amore, Age: 18

    6. Zoran Todorovitch, Age: 27

    After merging Simons li.size: 0 Removing simons

    Deque copied from list # Sequence:

    1. Melissa Robinson, Age: 9

    2. Joy Amore, Age: 18

    3. Zoran Todorovitch, Age: 27

    Vector after merging list and deque f Sequence:

    1. Melissa Robinson, Age: 9

    2. Joy Amore, Age: 18

    3. Melissa Robinson, Age: 9

    4. Joy Amore, Age: 18

    5. Zoran Todorovitch, Age: 27

    6. Zoran Todorovitch, Age: 27

    Deque after merging with list # Sequence:

    1. Melissa Robinson, Age: 9

    2. Joy Amore, Age: 18

    3. Zoran Todorovitch, Age: 27

    Генерирование последовательности
    С помощью алгоритма generate удобно создавать последовательности, имеющие строго определенную структуру. Например, если объявлен список целых из шести элементов, то его можно заполнить значениями, сгенерированными с помощью функции generate:

    //========= Создаем список целых

    list 1st (6);

    //========= Генерируем степенную последовательность

    generate (1st.begin (), Ist.end(), pows);
    pr(1st,"List of generated powers");

    Здесь pows — это внешняя функция, которая при каждом обращении возвращает возрастающую степень двойки. Эффект достигается за счет того, что static-переменная г инициализируется единицей во время компиляции, а затем помнит свое предыдущее значение:

    uint pows()
    {

    static uint r = 1;

    return r <= 1;
    }

    Если надо добиться обратного эффекта, то есть убрать закономерность в последовательности чисел, то можно воспользоваться шаблоном функции random_shuffle, которая переставляет элементы последовательности в одно из п! состояний. Например:
    vector v;

    for (int i = 0; i <= 6; i++ ) v.push_back(i+1);

    random_shuffle(v.begin() , v.end()) ;
    pr(v,"Vector of shuffled numbers");

    Последовательности типа vector

    Последовательности типа vector

    Для их использования необходимо подключить файл заголовков и сделать доступным (видимым) стандартное (std) пространство имен:

    #include using namespace std;

    Обратите внимание на отсутствие расширения .h в директиве подключения файла заголовков. Дело в том, что STL используется на множестве различных платформ с применением разных компиляторов. Файлы заголовков в разных условиях имеют разные расширения. Они могут иметь расширение Н, НРР или НХХ. Для того чтобы одинаково запутать всех разработчиков, было решено вовсе не использовать расширение для файлов заголовков библиотеки STL. Пространство имен std позволяет избежать конфликта имен. Если вы назовете какой-то свой класс тем же именем, что и класс из STL (например, string), то обращение вида std: : string однозначно определит принадлежность класса стандартной библиотеке. Директива using позволяет не указывать (многократно) операцию разрешения области видимости std: :, поэтому можно расслабиться и не писать std: : string, a писать просто — string.
    Вектор является шаблоном класса, который настраивается с помощью двух параметров:
    vectorallocator
    >
    Объект, который управляет динамическим выделением и освобождением памяти типа т, называется allocator. Для большинства типов контейнеров он обычно объявляется по умолчанию в конструкторе. Для «хитрых» данных, например требующих памяти в глобальном heap, видимо, можно изобрести индивидуальный распределитель памяти. Но в большинстве случаев работает вариант по умолчанию. Кроме того, с типом vector обычно связаны.4 типа сущностей:
  • Pointer — ведет себя как указатель на тип т;
  • const pointer — не позволяет изменить данные типа т, на которые он указывает;
  • reference — ссылки на данные типа т;
  • const reference— не позволяет изменить данные типа т, на которые она ссылается.
  • Обычно эти сущности являются тем, чем и должны являться, но это не гарантируется. Они могут быть более сложными объектами.

    Итак, vector является аналогом обычного массива в языке С, за исключением того, что он может автоматически изменять память по мере надобности. Доступ к данным обеспечивается с помощью операции выбора [ ]. Вставка новых элементов эффективна только в конец контейнера (push_back). Удаление — тоже. Данные операции в начале или середине влекут сдвиги всего массива данных, что резко снижает эффективность работы контейнера. Такие операции называются линейными, имея в виду тот факт, что время их выполнения линейно зависит от количества элементов в контейнере. Вставка или удаление в конце называются константными операциями, так как время их выполнения является константой для данной реализации и не зависит от количества элементов. Вот простая программа, иллюстрирующая использование вектора. Так как в приложениях консольного типа обычно возникают проблемы с русификацией, то для вывода текста мы используем английский язык:

    #include

    #include

    #include using namespace std;

    //======= Вводим тип для сокращения текста (места)

    typedef unsigned int uint;

    void main ()
    {
    //======== Вектор целых

    vector v(4);

    cout « "\nlnt Vector:\n";

    for (uint i=0; i
    {

    v[i] = rand()%10 + 1;

    cout « v[i] « "; ";
    }

    //======= Сортировка по умолчанию sort (v.begin (), v.end());

    cout « "\n\nAfter default sort\n";
    for (i=0; icout « v[i] « "; ";

    //======== Удаление элементов

    v.erase(v.begin());

    cout « "\n\nAfter first element erasure\n";
    for (i=0; i
    v. erase (v. end ()-2, v.endO);

    cout « "\n\nAfter last 2 elements erasure\n";

    for (i=0; i
    cout « v[i] « "; ";

    //======== Изменение размеров

    int size = 2; v.resize(size);

    cout « "\n\nAfter resize, the new size: " « v.size()

    « endl; for (i=0; i
    cout « v[i] « "; ";

    v.resize(6,-1);

    cout « "\n\nAfter resize, the new size: " « v.size()« endl;
    for (i=0; i
    cout « v[i] « "; ";

    //======== Статистика .

    cout « "\n\nVector's maximum size: " « v.max_size() « "XnVector's capacity: " « v.capacity() « endl

    //======== Резервирование

    v.reserve (100);

    cout « "\nAfter reserving storage for 100 elements:\n"

    « "Size: " « v.sizeO « endl :

    « "Maximum size: " « v.max_size() « endl

    « "Capacity: " « v.capacity() « endl;

    v.resize(2000);

    cout « "\nAfter resizing storage to 2000 elements:\n"

    « "Size: " « v.size() « end1

    « "Maximum size: " « v.max_size() « end1

    « "Capacity: " « v.capacity() « endl; cout « "\n\n";
    }

    Для того чтобы лучше уяснить смысл и различие методов size, resize, max_size и capacity, мы несколько раз повторяем вызовы этих методов. Если вы читаете книгу вдалеке от компьютера, то вам, возможно, будет интересно узнать, что программа выведет в окно консольного приложения:

    Int Vector:
    2; 8; 5; 1;

    After default sort
    1; 2; 5; 8;

    After first element erasure

    2; 5; 8;

    After last 2 elements erasure 2;

    After resize, the new size: 2,
    Vector capacity: 4 2; 0 ;

    After resize, the new size:
    6 2; 0; -1; -1; -1; -1;

    Vector's maximum size: 1073741823

    Vector's capacity: 6 After reserving storage for 100 elements:

    Vector's size: 6

    Vector's maximum size: 1073741823

    Vector's capacity: 100

    After resizing storage to 2000 elements:

    Vector's size: 2000

    Vector's maximum size: 1073741823

    Vector's capacity: 2000

    Шаблон функции вывода содержимого контейнера
    Демонстрация функционирования контейнеров требует часто выводить их содержимое, поэтому будет целесообразно написать шаблон функции, которая выводит содержимое контейнера любого типа. Первый параметр (т& v) функции рг () задает тип контейнера. Он же является параметром шаблона. Второй параметр (string s) задает строку текста (заголовок), который будет выведен в начале блока данных контейнера:

    //===== Шаблон функции для вывода с помощью итератора
    template void pr(T& v, string s)
    {
    cout«"\n\n\t"«s«" # Sequence: \n";

    //====== Итератор для любого контейнера

    Т::iterator p;

    int i;

    for (p = v.begin(), i=0; p != v.end(); p++, i++)

    cout « endl « i + 1 «". "« *p; cout « '\n';
    }
    Для пробега по всем элементам любого контейнера используется обобщенный, или абстрактный, указатель, который объявлен как т:: iterator. С помощью итератора, так же как и с помощью обычного указателя, можно получить доступ к элементу, используя операции *, ->. К нему также применима операция ++ — переход к следующему элементу последовательности, но в отличие от указателя с итератором не связана адресная арифметика. Вы не можете сказать, что значение итератора изменится на 4 байта при переходе к следующему элементу контейнера целых чисел, хотя для некоторых типов контейнеров это так и будет. Заметьте, что операция ++ в применении к итератору позволяет перейти к следующему элементу как вектора — элементы расположены в памяти подряд, так и списка — элементы расположены в памяти не подряд. Поэтому итератор — это более сложный механизм доступа к данным, чем простой указатель. Полезно представить итератор в виде рабочего, приставленного к контейнеру и призванного перебирать его элементы.

    Возможное присвоение p = v. end (); не означает, что итератор устанавливается на последний элемент последовательности. Вы помните, какую роль играет ноль для обычного указателя при работе с динамическим списком? Примерно такую же роль для итератора выполняет значение v. end () — конец последовательности. Его можно представить в виде итератора, указывающего на воображаемый элемент, следующий за последним элементом контейнера (past-the-end value). Однако инициализатор p = v.begin (); устанавливает итератор в точности на первый элемент последовательности.

    Вектор объектов класса
    Следующий фрагмент демонстрирует работу с вектором строк типа string. Теперь в контейнере будут храниться объекты класса string, который также является составной частью STL. Этот класс содержит ряд замечательных методов, которые позволяют легко и удобно работать со строками символов произвольной длины. В частности, вы можете складывать строки — осуществлять их конкатенацию, искать подстроки, удалять и заменять подстроки:

    #include

    #include

    #include // Для sort и distance

    #include // Для greater() tinclude

    using namespace std;

    void main ()
    {

    //========= Вектор строк текста

    vector v;

    v.push_back("pine apple");

    v.push_back("grape") ;

    v.push_back("kiwi fruit");

    v.push_back("peach") ;

    v.push_back("pear") ;

    v.push_back("apple") ;

    v.push_back("banana") ;

    //========= Вызываем наш шаблон вывода

    pr(v, "String vector");

    sort (v.begin () , v.end());

    pr(v, "After sort"); '

    //========= Изменяем порядок сортировки, на обратный

    //========= тому, который принят по умолчанию

    sort(v.begin(), v.end(), greater()) ;

    pr(v, "After predicate sort");

    cout « "\nDistance from the 1st element to the end: ";

    vector::iterator p = v.begin ();
    vector::difference_type d;
    d = distance(p, v.end());

    //========= Отметьте, что end() возвращает адрес

    //========= за концом последовательности

    cout « d « endl;

    cout « "\n\nAdvance to the half of that distanceXn";
    advance (p, d/2);

    cout « "Now current element is: " « *p « endl;
    d = distance(v.begin (), p);

    cout « "\nThe distance from the beginning: " « d « endl;

    d = distance(p, v.begin ());

    cout « "\nThe distance to the beginning: "

    « d « endl;
    }

    Здесь мы демонстрируем, как можно с помощью бинарного предиката greater <Туре> изменить порядок сортировки элементов последовательности. Предикатом называется функция, область значений которой есть множество { false, true } или { 0, 1 }. В нашем случае этот предикат пользуется результатом операции operator > (), определенной в классе string. Кроме того, мы показываем, как можно пользоваться шаблоном функций distance, который позволяет определить количество приращений типа dif ference_type между двумя позициями, адресуемыми итераторами. Другой шаблон функций — advance позволяет продвинуться вдоль контейнера на число позиций, задаваемое параметром, который может быть и отрицательным.

    Предикаты и функциональные объекты
    Предикатом, как определено в курсе математической логики, называется любая функция многих переменных, областью значений которой является множество {false, true} или {0, 1}. Покажем, как можно отсортировать по имени контейнер объектов класса Man, который мы определили в этом уроке выше. Алгоритм sort по умолчанию использует для сортировки бинарное отношение, задаваемое операцией operator< (). Так как в классе Man эта операция была определена в виде метода класса, то алгоритм справится с поставленной задачей. Однако если мы захотим изменить порядок и отсортировать последовательность объектов по возрасту, то нам придется воспользоваться другим отношением. Решить эту задачу можно двумя способами:

  • использовать свою собственную функцию-предикат, которая определяет порядок следования объектов;
  • использовать конструкцию, называемую функциональным объектом.
  • Первый способ реализуется путем создания глобальной функции, на вход которой поступают два сравниваемых объекта, а на выходе должен быть результат их сравнения, например типа bool. Второй способ реализуется созданием функционального объекта (function object или functor), являющегося структурой, в которой определена операция operator (). Этот термин, однако, используется для обозначения не только описанного объекта, но и для любого другого, который можно вызвать так, как вызывают функцию. Собственно, кроме описанного случая, роль функционального объекта может выполнять обычная функция и указатель на функцию.

    Покажем, как создать предикат. В описание класса Man следует добавить объявление внешней функции в качестве friend-объекта, так как в ее теле будут анализироваться private-данные класса Man. Добавьте в класс Man такое описание:

    //======== Предикат, задающий отношение порядка

    friend bool LessAge (Mans a, Man& b);

    Затем вставьте коды этой функции после объявления класса, но до тела функции main:

    bool LessAge (Man& a, Man& b)
    {

    //======== Сравниваем по возрасту

    return a.m_Age < b.m_Age;
    }

    Теперь можно создать контейнер объектов класса Man и убедиться в возможности его сортировки двумя способами. В момент создания контейнер может быть инициализирован элементами обычного массива. Ниже мы показываем, как это сделать:

    void main ()
    {
    //======== Массив объектов класса Man

    Man ar[] =
    {

    Man("Mary Poppins",36),

    Man("Joe Doe",30),

    Man("Joy Amore",18),

    Man("Zoran Todorovitch",27)
    };

    uint size = sizeof(ar)/sizeof(Man);

    //======== Создаем контейнер на основе массива

    vector men(ar, ar+size); pr(men,"Man Vector");

    //======== Реверсируем обычный массив

    reverse(ar, ar+size);

    cout « "\n\tAfter reversing the array\n\n";
    for (uint i=0; i
    cout « i+1 « ". " « ar[i] « '\n';

    //======== Сортиуем по умолчанию

    sort (men.begin (), men.endO);
    pr(men,"After default sort");

    //======== Используем предикат

    sort (men .begin () , men.endO, LessAge);
    pr(men,"After predicate LessAge sort");
    cout « "\n\n";
    }

    Алгоритм переворота последовательности (reverse) может работать как с контейнером, так и с обычным массивом. Для успешной работы ему надо задать диапазон адресов (range). Обратите внимание на то, что в качестве конца последовательности этому алгоритму, как и многим другим в STL, надо подать во втором параметре адрес ar+size, выходящий за пределы массива. Как объяснить тот факт, что шаблон функции reverse, требуя в качестве параметров переменные типа iterator, тем не менее работает, когда ему подают обычный указатель типа Man*? В документации вы можете найти такое объяснение. Указатель — это итератор, но итератор — это не указатель. Итератор — это обобщение (generalization) указателя.

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

    Man Vector # Sequence:

    1. Mary Poppins, Age: 36

    2. Joe Doe, Age: 30

    3. Joy Amore, Age: 18

    4. Zoran Todorovitch, Age: 27

    After reversing the array

    1. Zoran Todorovitch, Age: 27

    2. Joy Amore, Age: 18

    3. Joe Doe, Age: 30

    4. Mary Poppins, Age: 36

    After default sort # Sequence:

    1. Joe Doe, Age: 30

    2. Joy Amore, Age: 18

    3. Mary Poppins, Age: 36 /

    4. Zoran Todorovitch, Age: 27

    After predicate LessAge sort # Sequence:

    1. Joy Amore, Age: 18

    2. Zoran Todorovitch, Age: 27

    3. Joe Doe, Age: 30

    4. Mary Poppins, Age: 36

    Здесь мы убрали сообщения, которые выводит деструктор класса Man. Они носят чисто учебный характер. В частности, они позволяют обозначить те моменты из жизни объектов класса Man, когда они уничтожаются, то есть когда система вызывает для них деструктор.

    Сейчас мы намерены создать функциональный объект и использовать его для выбора режима сортировки по имени или по возрасту. Текущий выбор удобно хранить в static-переменной класса Man. Такие переменные, как вы знаете, являются общими для всех объектов класса. Изменяя их значение, можно управлять общими установками, касающимися всех объектов класса. Мы будем управлять текущим режимом сортировки. Для удобства чтения кода введем глобальное определение типа SORTBY - перечисление режимов сортировки:

    enum SORTBY { NAME, AGE }; // Режимы сортировки

    Декларацию static-переменной следует вставить в public-секцию класса Man:

    static SORTBY m_Sort; // Текущий режим сортировки

    Определение static-переменной, согласно законам ООП, должно быть глобальным:

    //======= Определение и инициализация

    SORTBY Man::m_Sort = NAME;

    Сам функциональный объект должен быть объявлен как внешняя глобальная friend-конструкция. Вставьте следующее объявление внутрь класса Man:

    //======= Функциональный объект имеет доступ к данным

    friend struct ManLess;

    Затем объявите сам функциональный объект, который, надо признать, имеет несколько непривычный вид:

    //======== Функциональный объект

    struct ManLess
    {

    bool operator()(Man& a, Man& b)
    {

    return a.m_Sort==NAME ? (a.m_Name < b.m_Name)

    : (a.m_Age < b.m_Age);
    }
    };

    Как вы видите, он имеет вид структуры (возможен и класс), в которой определена единственная операция operator (). В нем мы анализируем текущее значение флага сортировки и в зависимости от него возвращаем результат сравнения двух объектов, поступивших в качестве параметров, по тому или иному полю. Использование функционального объекта иллюстрируется следующим кодом, который вы можете вставить в конец существующей функции main:

    //========= Используем функциональный объект

    Man::m_Sort = NAME;

    //========= Сортируем по имени

    sort (men .begin (), men.end(), ManLess ());
    pr(men,"After function object name sort");

    Man::m_Sort = AGE;

    //========= Сортируем по возрасту

    sort (men. begin (), men.end(), ManLess ());
    pr(men,"After function object age sort");

    Аналогично предикату greater, который мы уже использовали, в STL определен предикат less, который обеспечивает упорядочивание контейнера, задаваемое операцией operator< (). Но, если вы вставите в функцию main такой код:

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

    sort(men.begin(), men.end(),less() ) ;
    pr(men,"After less sort");

    то получите сообщение об ошибке, так как он будет искать реализацию operator< () в виде внешней функции с двумя сравниваемыми параметрами. Напомню, что мы уже реализовали эту операцию, но в виде метода класса с одним параметром. Для решения проблемы вы можете, не убирая старой версии, вставить новую. Декларируйте в классе Man внешнюю friend-функцию:

    //========= Нужна для предиката less()

    friend bool operator< (const Man& a, const Man& b);

    Затем дайте внешнее тело этой функции. Отношение порядка здесь намеренно изменено по сравнению с предыдущей реализацией operators (). Как оказалось, обе версии будут работать в различных ситуациях. Первая — при сортировке по умолчанию, а вторая — при сортировке предикатом less .

    bool operator<(const Man& a, const Man& b)

    {

    //======== Сравниваем по возрасту

    return a.m_Age < b.m_Age;
    }

    Проверьте результат, запустив приложение. Проследите, чтобы в main был при этом код с вызовом алгоритма сортировки с тремя Параметрами:

    sort(men.begin (), men.end(),less());

    Здесь же уместно добавить, что в STL есть шаблоны, которые называются negators (отрицатели). Шаблон not2, например, позволяет инвертировать результат бинарной операции. Вставьте в конец функции main следующий фрагмент:

    //========= Используем отрицатель бинарной операции

    sort(men.begin (), men.endf), not2 (less()));
    pr(men,"After not2(less) sort");
    и убедитесь в том, что последовательность отсортирована теперь по убыванию возраста.

    ы использования string

    Примеры использования string

    Тип string является специализацией шаблона basic_string для элементов типа char и определен как:
    typedef basic_string string;

    Шаблон basic_string предоставляет типы и методы, схожие с теми, что предоставляют стандартные контейнеры, но он имеет много специфических методов, которые позволяют достаточно гибко манипулировать как строками, так и их частями (подстроками). Минимизация операций копирования строк, которой гордится MFC-класс cstring, на самом деле приводит к труднообнаруживаемым и невоспроизводимым (irreproducible) ошибкам, которые очень сильно портят жизнь программистам. Я с интересом узнал, что члены комиссии по утверждению стандарта C++ анализируют ошибки, возникающие из-за совместного использования двумя переменными строкового типа одной и той же области памяти, и пытаются выработать спецификации относительно времени жизни ссылок на символы строки. Если вы запутались в этой фразе, то следующий фрагмент программы, который комиссия использует в качестве теста, должен прояснить ситуацию. При выполнении он выведет строку «Wrong» или «Right», что означает, что ваша реализация string ненадежна или, скорее всего, надежна. Если она выведет строку «Right», то это еще не означает, что ваша реализация надежна. Ошибки могут всплыть в многопоточных приложениях, когда разные потоки работают с одной строкой символов:

    //====== Две тестовые текстовые строки

    string source("Test"), target;

    //====== Ссылка на второй символ в строке

    char& с = source[1];

    //=====- Если данные не копируются при присвоении

    target = source;

    //====== то это присвоение изменит обе строки

    с = ' z ' ;

    //====== Этот тест позволяет выяснить ситуацию

    cout « (target[l] == 'z1 ? "\nWrong" : "\nRight");

    Здесь мы использовали ссылку, но аналогичное поведение обнаруживает и итератор. Вы можете объявить и использовать его так:

    string::iterator it = source.begin()+1; *it = z1 ;

    В рассматриваемой версии Studio.Net я с удовлетворением отметил, что тест выводит строку «Right». Следующий фрагмент демонстрирует технику обрезания «пустого» текста в начале и конце строки. Она не очень эффективна, но вполне пригодна для строк небольшого размера:

    //====== Множество пустых символов

    char White " \n\t\r";

    //====== Ищем реальное начало строки

    //====== и усекаем лишние символы слева

    s = s.substr(s.find_first_not_of(White));

    //====== Переворачиваем строку и повторяем процедуру

    reverse (s .begin () , s.endO);

    s = s.substr(s.find_first_not_of(White));

    //====== Вновь ставим строку на ноги

    reverse (s .begin (), s.end());

    Интересный пример, иллюстрирующий работу со строками, я увидел в MSDN. Нечто вроде секретного детского языка под названием Pig Latin (свинячья латынь). Алгоритм засекречивания слов состоит в том, что от каждого слова отрывают первую букву, переставляют ее в конец слова, а затем добавляют туда окончание «ау». Игра, очевидно, имеет свою историю. Приведем коды функции, которая реализует этот алгоритм и возвращает засекреченную строку:

    //====== Преобразование строки по принципу Pig Latin

    string PigLatin (const strings s)
    {
    string res;

    //======= Перечень разделителей слов

    string sep(" .,;:?");

    //======= Длина всей строки

    uint size = s.lengthO;

    for (uint start=0, end=0, cur=0; cur < size; cur=end+l)
    {

    //==== Ищем позицию начала слова, начиная с cur

    start = s.find_first_not_of(sep, cur) ;
    //==== Копируем разделители между словами
    res += s.substr(cur, start - cur) ;
    //==== Ищем позицию конца слова, начиная со start
    end = s.find_first_of(sep, start) ;
    //==== Корректируем позицию конца слова
    end = (end >= size) ? size : end - 1 ;
    //==== Преобразуем по алгоритму

    res += s. substr (start-t-1, end-start) + s [start] +"ay"; )

    return res;
    }

    Проверьте работу алгоритма с помощью следующего теста, который надо вставить внутрь функции main:

    string s("she,sells;
    sea shells by the sea shore");
    cout « "Source string: " « s « endl;
    cout « "\nPig Latin(s): " « PigLatin(s);

    В результате вы увидите такой текст:

    Source string: she,sells;
    sea shells by the sea shore

    Pig Latin(s): hesay,ellssay;
    easay hellssay ybay hetay easay horesay

    Работа с потоками

    Работа с потоками

    Шаблон класса if stream позволяет работать с файловыми потоками и производить ввод объектов произвольного типа. Удобно вводить объекты прямо в контейнер. Специальный итератор (istream_iterator) помогает в этом случае воспользоваться алгоритмами (например, сору). При достижении конца потока (end of stream) итератор принимает специальное значение, которое служит барьером выхода за пределы потока (past-the-end iterator). В примере, приведенном ниже, используется еще один тип итератора (back_insert_iterator). Он является адаптером, позволяющим вставлять элементы в конец последовательности. Если использовать прямой inserter, то при чтении из файла последовательность будет реверсирована (перевернута). Позиционирование в потоке осуществляется с помощью метода seekg, техника использования которого также демонстрируется в примере:

    void main ()
    {

    //========== Вектор строк

    vector v;

    v.push_back("Something in the way ");

    v.push_back("it works distracts me ");

    v.push_back("like no other matter");

    pr(v,"Before writing to file");

    //========== Запрашиваем имя файла

    cout « "\nEnter File Name: ";
    string fn, text; cin » fn;

    //========== Приписываем расширение

    int pos = fn.rfind(".");

    if (pos > 0)

    fn.erase(pos);

    fn += ".txt";

    //========== Создаем и открываем поток

    ofstream os(fn.c_str());

    //========== Определяем входной и выходной потоки

    typedef istream_iteratorchar,

    char_traits > Strln;
    typedef ostream_iteratorchar,

    char_traits > StrOut;

    //========== Копируем контейнер в выходной поток

    copy (v.begin(), v.end(), StrOut(os,"\n"));
    os.close();

    //========== Открываем файл для чтения

    if stream is(fn.c_str());

    //========= Пропуск 17 символов

    is.seekg(17) ;
    is » text;

    cout « "\n\nStream Positioning:\n\n" « "17 bytes:\t\t" « text « endl;

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

    is.seekg(0, ios_base::beg);

    is » text;

    cout « "0 bytes:\t\t" « text « endl;

    //========== Сдвигаем на 8 символов от конца

    is.seekg(-8, ios_base::end);

    is » text;

    cout « "-8 bytes from end:\t" « text « "\n\n";

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

    is.seekg(0, ios_base::beg);
    v.clear () ;

    //========== Копируем в контейнер

    copy(Strln(is),Strln(),back_inserter(v));
    pr(v,"After reading from file");
    cout«"\n\n"; }

    Программа производит следующий выход:

    Before writing to file # Sequence:

    1. Something in the way

    2. it works distracts me

    3. like no other matter

    Enter File Name: test

    Stream Positioning:

    17 bytes: way

    0 bytes: Something

    -8 bytes from end: matter

    After reading from file # Sequence:

    1. Something

    2. in

    3. the

    4. way

    5. it

    6. works

    7. distracts

    8. me

    9. like

    10. no

    11. other

    12. matter

    Сечения массива

    Сечения массива

    Проблемы оптимизации работы с матрицами давно волнуют создателей компиляторов. В то далекое время, когда решения задач электродинамики и вообще краевых задач матфизики еще интересовали влиятельных людей нашей страны (скорее, научные авторитеты убеждали их, что такие задачи следует решать), мы, используя язык PL/I или FORTRAN, конечно же, хранили и обрабатывали матрицы в одномерных массивах. Дело в том, что выбор одного элемента из более естественного для матриц двухмерного массива обходился дорого. Выработалась особая техника работы с одномерными массивами, хранящими матрицы (обычно разреженные). В языке C++ операция выбора элемента из двухмерного динамического массива не намного дороже, чем из одномерного (да и скорости изменились), поэтому острота проблемы спала. Тем не менее проблема экономии времени при решения сложных краевых задач не ушла в прошлое.

    STL имеет пару вспомогательных классов: slice и gslice, которые созданы для того, чтобы было удобно работать со срезами (сечениями) одномерных массивов. Если вы храните двухмерную матрицу в последовательности типа valarray, то элементы одной строки матрицы или одного ее столбца можно представить в виде сечения, то есть определенной части всей последовательности. Конструктор класса slice определяет закономерность, в соответствии с которой будут выбираться элементы последовательности, чтобы образовать срез. Например, объект slice s(0, n , 2); представляет собой сечение из п элементов последовательности. Элементы выбираются начиная с нулевого, через один, то есть с шагом 2. Если вы храните матрицу пхп в последовательности типа valarray и при этом она упорядочена по строкам (сначала первая строка, затем вторая, и т. д.), то третью строку матрицы можно выбрать с помощью сечения:

    slice s (2*n, n , 1);

    Действительно, параметры указывают, что надо пропустить 2*n элементов, затем выбрать n элементов с шагом по одному. Если матрица хранится a la FORTRAN, то есть по столбцам, то для выбора той же строки надо определить сечение:

    slice s (2, n , n);

    Пропускаются два элемента, затем выбирается n элементов с шагом п. Вы, конечно, поняли, как создать сечение, но не поняли, какое отношение оно имеет к последовательности valarray, так как она не фигурирует в приведенных выражениях. Да, синтаксис, связывающий срез с valarray, несколько необычен, хотя вполне логичен:

    int n = 5, // Размерность матрицы n (размером пхп) пп = п*п;
    // Размерность valarray

    //=== Создаем матрицу (одномерную последовательность)
    valarray a (nn);

    //=== Генерируем ее элементы по закону f (Пока его нет)
    generate (&a[0], &a[nn], f) ;

    //====== Создаем сечение

    slice s (0, n , 1);

    //====== Выделяем сечение (первую строку,

    //====== если матрица хранится по строкам)

    valarray v = a[s];

    Вы видите, что объект s класса slice помещается в то место, куда мы обычно помещаем целочисленный индекс массива или последовательности. Такая интерпретация операции [ ] непривычна. Вы, вероятно, догадались, что роль объекта s в приведенном фрагменте является чисто эпизодической. Можно обойтись и без него, заменив его временным безымянным объектом, который создаст компилятор. При этом конструкция выражения будет более эффективной, но и более головоломной. Последние две строки фрагмента можно заменить одной строкой:

    valarray v = afslice (0, n , 1);

    Подведем итоги. В этом уроке мы оценили возможности библиотеки STL и сделали вывод, что она, очевидно, имеет гораздо больше достоинств, чем недостатков. Необходимо регулярно тренировать технику ее использования. В этой задаче может помочь сеть Интернет, в которой появляется все больше сайтов, уделяющих внимание STL. Кроме того, мы:

  • вспомнили, как создавать шаблоны функций и шаблоны классов;
  • узнали, что стандартные контейнеры делятся на последовательности и ассоциативные контейнеры;
  • узнали, как пользоваться предикатами и функциональными объектами;
  • познакомились с возможностями связывателей, адаптеров и отрицателей;
  • узнали, как шаблоны пар помогают работать с ассоциативными контейнерами типа тар;
  • получили представление об использовании очередей и стека;
  • оценили возможности текстовых строк типа string;
  • научились пользоваться итераторами различного типа, в том числе и для управления потоками ввода-вывода;
  • узнали о наличии большого количества полезных констант;
  • поработали с последовательностями типа valarray и их сечениями;
  • опробовали некоторые алгоритмы управления последовательностями.


  • Шаблон функции быстрой сортировки

    Шаблон функции быстрой сортировки

    Приведем пример реализации вышеупомянутого рекурсивного алгоритма сортировки массива переменных Quicksort. Его идея состоит в том, что меняются местами элементы массива, стоящие слева и справа от выбранного «центрального» (mid) элемента массива, если они нарушают порядок последовательности. Интервал, в котором выбирается центральный элемент, постепенно сжимается, «расправляясь» сначала с левой половиной массива, затем с правой. Функция Quicksort, приведенная ниже, реализует рекурсивный алгоритм быстрой сортировки. Далее следует код, который позволяет протестировать работу функции. Сортируется массив вещественных чисел, элементы которого заданы случайным образом:

    void Quicksort (double *ar, int 1, int r)
    {

    //========== Рабочие переменные

    double mid, temp;

    //========== Левая и правая границы интервала

    int i = 1, j = r;

    //========== Центральный элемент

    mid = ar[ (1 + г) /2];

    //========== Цикл, сжимающий интервал

    do

    //== Поиск индексов элементов, нарушающих порядок
    while (ar[i] < mid)

    i++; // слева
    while (mid < ar[j])

    j--; // и справа

    //== Если последовательность нарушена,

    if (i <= j)

    {

    //===== то производим обмен

    temp = ar[i];
    ar[i++] = ar[j];
    ar[j-—] = temp;
    }
    }
    //========= Цикл do-while повторяется, пока

    //========= есть нарушения последовательности

    while (i <= j);

    //========= Если левая часть не упорядочена,

    if (I < j)

    Quicksort (ar, 1, j); // то занимаемся ею

    // Если правая часть не упорядочена,
    if (i < r)

    Quicksort (ar, i, r); // то занимаемся ею }

    //========== Тестируем алгоритм

    void main()
    {

    //========= Размер массива сортируемых чисел

    const int N = 21;

    double ar[N]; // Сам массив

    puts("\n\nArray before Sorting\n");

    for (int i=0; i {

    ar[i] = rand()%20;

    if (i%3==0)

    printf ("\n");

    printf ("ar[%d]=%2.0f\t",i,ar[ij);
    }

    Quicksort(ar,0,N-1); // Сортировка

    puts("\n\nAfter SortingNn");

    for (i=0; i
    {

    if (i%3==0)

    printf ("\n");

    printf ("ar[%d]=%2.0f\t",i,ar[i]);
    }

    puts ("\n");
    }

    Для того чтобы сортировать массивы любого типа, целесообразно на основе данной функции создать шаблон. Оказывается, что для этого нужно приложить совсем немного усилий. В уже существующий код внесите следующие изменения:

    template T>

    void Quicksort (Т *ar, int 1, int r)
    {

    //======= Рабочие переменные

    Т mid, temp;

    //======= Далее следует тот же код, который приведен

    //======= в оригинальной версии функции Quicksort

    }

    Проверьте функционирование, вставив в функцию main вызовы функции с другими типами параметров. Например:

    void main()
    {

    //======= Размер массива сортируемых чисел

    const int N = 21;

    // double ar[N];

    int ar[N];

    puts("\n\nArray before SortingXn");

    for (int i=0; i
    {

    ar[i] = rand()%20; if (i%3==0)

    printf ("\n"); // printf ("ar[%d]=%2.0f\t",i,ar[i]);

    printf ("%d\t",ar[i]);
    }

    Quicksort(ar,0,N-1);

    puts("\n\nAfter SortingXn");

    for (i=0; i
    {

    if (i%3==0)

    printf ("\n"); // printf ("ar[%d]=%2.0f\t",i,ar[i]);

    printf ("%d\t",ar[i]);
    }

    puts("\n");
    }

    В данный момент функция main настроена на сортировку массива целых. Внесите приведенные изменения и проверьте работу шаблона для массива целых. Уберите комментарии там, где они есть, но вставьте комментарии в строки программы, следующие ниже. После этой процедуры функция main будет настроена на проверку шаблона функции сортировки для массива вещественных. Проверьте и этот случай. Шаблон должен справиться с обоими.

    Примечание
    Примечание

    Перед запуском консольных приложений настройте консольное окно так, чтобы его размеры вмещали весь выходной текст. Для этого вызовите контекстное меню на заголовке консольного окна и дайте команду Properties. Откройте страницу на вкладке Layout и подстройте размеры окна в полях Width и Height группы Window Size.

    Шаблон классов valarray

    Шаблон классов valarray

    Этот шаблон разработан для оптимизации вычислений, производимых над массивами чисел фиксиррванного размера. Valarray похож на контейнер, но он им не является. Вы не можете динамически и эффективно наращивать его размер. Он, как и контейнер, может изменять свои размеры, используя метод resize, но при этом имеющиеся данные разрушаются. Главным преимуществом использования valarray является эффективность проведения операций сразу над всеми элементами последовательности. Предположим, вы хотите построить график функции у = sin(x) и имеете процедуру, которая сделает это с учетом масштабирования, оцифровки осей и всяких других удобств. Вашей задачей является лишь сформировать данные для графика и подать их на вход этой процедуры. Использование valarray даст преимущество в легкости манипулирования данными и эффективности выполнения. Для простоты выберем шаг изменения координаты х, равный л/3,

    Примечание
    Примечание

    C целью экономии места я обычно не привожу директивы препроцессора, которые, конечно же, должны предшествовать каждому из рассматриваемых фрагментов. Большинство читателей, я уверен, успешно решают эту проблему сами, так как сообщения об ошибках обычно довольно ясно указывают на недостающее описание. Но при работе с библиотекой STL окно сообщений ведет себя не совсем так, как при работе с MFC. Незначительный пропуск или неточность со стороны программиста порой приводят к лавине предупреждений и ошибок, анализ которых превращается в испытание для нервной системы. Здесь у компании Microsoft еще довольно много работы. Учитывая сказанное, следующий фрагмент приведен со списком директив, необходимых для его работы.

    #include

    #include

    #include

    #include

    using namespace std;

    void main() { //======== Вспомогательные переменные

    double PI = atan(l.)*4.,

    dx = PI/3., // Шаг изменения
    xf = 2*PI - dx/2.;
    // Барьер

    int i = 0,

    size = int(ceil(xf/dx)); // Количество точек

    //======== Создаем два объекта типа valarray

    valarray vx(size), vy(size);

    //======== Абсциссы точек вычисляются в цикле
    for (double х=0.;
    х < xf; х += dx) vx[i++] = х;

    //======== Ординаты вычисляются без помощи цикла

    vy = sin(vx);

    cout«"Valarrays of x and sin(x)\n";
    for (i=0; i < size; i++)

    cout«"\nx = " « vx[i] «" у = "« vy[i];
    }

    Теперь усложним задачу. Представим, что надо численно продифференцировать функцию, заданную в дискретном множестве точек. Вы знаете, что конечные разности позволяют аппроксимировать производные, то есть производить численное дифференцирование. В STL есть алгоритм adjacent_dif ference, который вычисляет первые конечные разности в указанном диапазоне последовательности. Здесь важно вспомнить, что valarray не является контейнером и поэтому не поддерживает итераторов. Но алгоритмы STL принимают в качестве аргументов как итераторы, так обычные указатели. Мы воспользуемся этим фактом, а также тем, что элементы valarray расположены в памяти подряд.
    Результат дифференцирования надо поместить в другую последовательность типа valarray, которую после этого можно эффективно нормировать, поделив сразу все ее элементы на шаг дискретизации вдоль оси х. Добавьте директиву # include , вставьте следующий текст в конец предыдущего фрагмента и, пожалуй, увеличьте количество точек, заменив присвоение dx = Pi/З. на dx = Pi/10:

    //======= Конструктор создает valarray нужного размера

    valarray vd(size);

    //======= Алгоритм вычисляет конечные разности

    adjacent_difference(&vy[0], &vy[size], &vd[0]);

    //======= Все элементы valarray делятся на dx

    vd /= dx;

    //======= Мы проверяем результат

    cout«"\n\nValarray of differences\n";
    for (i=l; i < size; i++)
    cout«"\nx = " « vx[i] «" у = "« vd[i];

    Отметьте, что в первой точке (с нулевым индексом) будет ошибка, поэтому мы ее не выводим. Остальные элементы результирующей последовательности чисел (valarray vd) должны вести себя как у = cos(x). В качестве третьего параметра функции adjacent_dif ference нельзя задать просто vd, так как в отличие от обычного массива имя vd не является адресом его первого элемента. Шаблон классов valarray имеет некоторое, весьма ограниченное количество методов, которые позволяют производить манипуляции с данными, среди которых стоит отметить: min, max, sum, shift, cshift, apply. Приведем фрагмент, иллюстрирующий их использование:

    //======= Функциональный объект, применяемый к каждому

    //======= элементу valarray

    double Sharp (double x)
    {

    return x != 0. ? l/(x*x) : DBL_MAX;
    }

    //======= Функция для вывода valarray

    void out(char* head, valarray& v)
    {

    cout « '\n' « head << '\n';

    for (unsigned i=0; i < v.size(); i++)

    cout«"\nv[" « i « "] = " « v[i];

    cout «'\n';
    }

    void main()
    {

    int size = 11;

    valarray vx(size), vy(size);

    //======== Заполняем диапазон от -1 до 1

    for (int i=0; i < size; i++)
    {

    vx[i] = i/5. - 1.;
    }

    out("Initial valarray", vx);

    //======== Вычисляем сумму всех элементов

    cout « "\nsum = " « vx.sum() « endl;

    //======== Применяем свое преобразование

    vy = vx.apply (Sharp);

    //======== Получили "острую" функцию

    out("After apply", vy);

    //======== Вычисляем min и max
    cout « "\n\nmin = " « vy.min() « " max = " « vy.max();
    }

    При положительных значениях аргумента метод shift используется для сдвига всей последовательности влево или при отрицательных значениях — вправо. Метод cshif t представляет собой циклическую модификацию метода shift. Заметьте, что все рассмотренные методы возвращают новую последовательность типа valarray и не имеют модификаций, работающих в режиме in-place, что, на мой взгляд, является ощутимым недостатком этого типа данных. Вы можете проверить работу сдвигов, добавив такие строки:

    //======== Циклический сдвиг на 2 позиции влево

    valarray r =vy.cshift(2);

    out("After cyclic 2 digits left shift", r) ;

    //======== Сдвиг на 2 позиции вправо

    r =r.shift(-2);

    out("After 2 digits right shift", r);

    Шаблоны классов

    Шаблоны классов

    Шаблон классов (class template) в руководствах программиста иногда называется generic class или class generator. Шаблон действительно помогает компилятору сгенерировать определение конкретного класса по образу и подобию заданной схемы. Разработчики компилятора C++ различают два термина: class template и template class. Первый означает абстрактный шаблон классов, а второй — одно из его конкретных воплощений. Пользователь может сам создать template class для какого-то типа данных. В этом случае созданный класс отменяет (overrides) автоматическую генерацию класса по шаблону для этого типа данных. Рассмотрим стандартный пример, иллюстрирующий использование шаблона для автоматического создания классов, которые реализуют функционирование абстрактного типа данных «Вектор линейного пространства». Элементами вектора могут быть объекты различной природы. В примере создаются векторы целых, вещественных, объектов некоторого класса circle (вектор окружностей) и указателей на функции. Для вектора из элементов любого типа тела методов шаблона одинаковы, поэтому и есть смысл объединить их в шаблоне:

    #include

    #include

    #include

    //====== Шаблон классов "Вектор линейного пространства"

    template T> class Vector

    {

    //====== Данные класса

    private:

    Т *data; // Указатель начала массива компонентов

    int size; // Размер массива

    //====== Методы класса

    public:

    Vector(int);

    ~Vector()
    {
    delete[] data;
    }

    int Size()
    {
    return size;
    }

    T& operator [](int i)
    {
    return data[i];
    }
    };

    //====== Внешняя реализация тела конструктора

    template T> Vector::Vector(int n)
    {

    data = new Т[n];

    size = n; };

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

    class Circle

    {

    private:

    //====== Данные класса

    int х, у; // Координаты центра

    int r; // Радиус

    public:

    //====== Два конструктора

    Circle ()
    {

    х = у = r = 0; }

    Circle (int a, int b, int с) {

    x = a;

    У = b;

    r = с;
    }
    //====== Метод для вычисления площади круга

    double area ()
    {

    return 3.14159*r*r;
    }
    };

    //====== Глобальное определение нового типа

    //====== указателя на функцию

    typedef double (*Tfunc) (double);

    //====== Тестирование ч

    void main ()
    {

    //===== Генерируется вектор целых

    Vector x(5) ;

    for ( int i=0; i < x.SizeO; ++i)

    {

    x[i] = i; // Инициализация
    cout « x[i] « ' ' ; // Вывод

    }

    cout « ' \n ' ;

    //===== Генерируется вектор вещественных Vector y(10);

    for (i=0; i < y.SizeO; ++i)

    {

    y[i] = float (i); // Инициализация cout « y[i] « ' ' ; // Вывод

    }

    cout « ' \n' ;

    //==== Генерируется вектор объектов класса Circle
    Vector z(4);

    for (i=0; i< z.SizeO; ++i) // Инициализация

    z[i] = Circle(i+100,i + 100,i+20) ;

    cout « z[i].area() « " "; // Вывод
    }
    cout « ' \n' ;

    //==== Генерируется вектор указателей на функции

    Vector f(3);

    cout«"\nVector of function pointers: " ;

    f[0] = sqrt; // Инициализация

    f[l] = sin;

    f[2] = tan;

    for (i=0; i< f.Size(); ++i)

    cout « f[i](3.) « ' '; // Вывод cout « "\n\n";
    }

    Обратите внимание на синтаксис внешней реализации тела конструктора шаблона классов. Vector — это имя шаблона, a Vector (int n) — имя метода шаблона (конструктор). При использовании шаблона для генерации конкретного вектора объектов необходимо задать в угловых скобках тип данных, известный к этому моменту и видимый в этой области программы. Использование шаблона всегда предполагает наличие описателя типа при имени класса (Vector ). Имя Vector теперь не может быть использовано без указания конкретного типа элементов.

    В рассмотренном примере операция [ ] определена в шаблоне как общая для всех типов Т, однако метоД area () определен только для объектов класса Circle и он применяется к объекту z [i] класса circle, вектор из четырех элементов которого автоматически создается компилятором при объявлении Vector z (4);. Работая с вектором указателей на функции, мы в цикле по переменой i вызываем i-ю функцию и посылаем ей в качестве аргумента вещественное число 3 (см. вызов f [i] (3.) ).

    Если для какого-то типа переменных автоматически сгенерированный по шаблону класс не подходит, то его следует описать явно. Созданный таким образом класс (template class) отменяет автоматическое создание класса по шаблону только для этого типа. Например, предположим, что создан новый класс Man:

    class Man

    {
    private:

    string m_Name;
    int m_Age;

    public:

    //======= Конструкторы

    Man{}
    {

    m_Name = "Dummy";

    m_Age = 0; }

    Man (char* n, int a)

    {

    m_Name = string(n); m_Age = a;
    }

    Man (strings n, int a)
    {

    m_Name = n;

    m_Age = a;
    }

    Man& operator=(const Man& m)
    {

    m_Name = m.m_Name;

    m_Age = m.m_Age;

    return *this;
    }

    Man(const Man& m)
    {

    *this = m;

    }

    //======== Деструктор

    ~Man()
    {

    cout « "\n+ + " « m_Name « " is leaving";

    m_Name.erase (); }

    bool operator==(const Man& m)
    {

    return m_Name == m.m_Name;
    }

    bool operator<(const Mans m)

    {

    //======= Упорядочиваем по имени

    return m_Name < m.m_Name;

    }

    friend ostreams operator«(ostreams os, const Mans m);
    };

    //========= Внешняя реализация операции вывода

    ostream& operator«(ostreams os, const Mans m)
    {

    return os « m.m_Name « ", Age: " « m.m_Age;
    }

    Для класса Man мы не хотим использовать class template Vector, но хотим со здать вектор объектов класса, работающий несколько иначе. С этой целью явн описываем новое конкретное воплощение (template class) класса Vector дл. типа Man.

    class Vector
    {

    Т *data;

    int size;

    public:

    Vector (int n, T* m);

    ~Vector 0 { delete [] data;
    }

    int Size()
    {
    return size;
    }

    T& operator [] (int i)
    {
    return data[i];
    }
    };

    Vector : : Vector (int n, T* m)
    {

    size = n;

    data = new Man [n] ;

    for (int i=0; i
    data [i] = m[i] ;
    }

    Отличие от шаблона состоит в том, что конструктор класса vector имеет теперь два параметра, а не один, как было в шаблоне. Теперь массив указателей data инициализируется данными массива объектов, поданного на вход конструктора. Цель этого нововведения — показать технику частного воплощения шаблона. Для проверки функционирования вектора из элементов типа Man следует создать какой-то тестовый массив, например:

    Man miles ("Miles Davis", 60); // Отдельный Man

    //====== Массив объектов класса Man

    Man some [ ] =

    {

    Man("Count Basis", 70),

    Man ("Duke Ellingtcnton", 90) ,

    miles,

    Man("Winton Marsales", 50) ,
    };

    а в функцию main ( ) добавить манипуляции с реальным вектором типа Man. В коде, который приведен ниже, обратите внимание на то, что при создании вектора men из четырех объектов класса Man вторым параметром передается адрес обычного массива объектов, инициализирующий все элементы (внутреннего для шаблона) массива data:

    //====== Конструируем вектор объектов

    //====== на основе массива объектов

    Vector men (sizeof (some) /sizeof (Man) , some);
    cout«"\nVector of Man: ";

    //====== Вывод вектора

    for (i=0; i< men. Size (); ++i)
    cout « men[i] « "; ";

    В шаблоне классов могут быть объявлены static данные и функции. При этом следует иметь в виду, что каждый конкретный класс, образованный по шаблону,

    будет иметь свои собственные копии static членов. Эти члены будут общими для всех объектов конкретного класса, но различными для всех классов — реализаций шаблона.

    Параметры шаблона
    При описании шаблона можно задать более одного параметра. Например:

    template T, int size=256> class Stack {...};

    Теперь при создании конкретной реализации класса можно задать размер стека

    Stack 2048>;

    или
    Stack 8*N>;

    Важно запомнить, что числовой параметр должен быть константой. В примере переменная N могла быть описана как const int N=1024; но не могла быть переменной int N=1024;. При создании конкретного класса по шаблону возможно вложенное определение класса, например, если был описан частный случай класса — шаблон структур вида:

    template T> struct Buffer {...};

    то после этого можно создать конкретную структуру, в качестве типа которой задать структуру, созданную по этому же шаблону, например:

    Buffer > buf;


    Между двумя закрывающими угловыми скобками » надо вставить символ пробела, так как в языке C++ операция >> имеет самостоятельный смысл, и не один. Существует возможность генерировать по шаблону классы, которые являются производными от какого-то базового класса. Например, если описать базовый класс TList, в котором не определен тип элементов хранения, то есть используется тип void, то целесообразно ввести описание шаблона производных классов:

    class TList

    //======== Начало списка

    void *First;:

    public:

    void insert (void*);

    int order (void*, void*, int);

    //======== Другие методы

    };

    template T> class List :
    public TList T *First;

    public:

    void insert (T *t)
    {
    TList::insert(t);
    }
    int order (T *pl, T *p2, int n)

    {

    return TList::order(pi, p2, n);
    }
    //======= Другие методы

    };

    В этих условиях становится возможным декларация списка, состоящего из элементов одного определенного типа, например List intList;, или гетерогенного списка, состоящего из элементов различных типов, образованных от какого-то одного базового класса. Например, объявление List DevList; генерирует класс для работы со списком приборов, из иерархии классов Device, то есть в списке могут быть объекты классов, производных от Device. Аналогичный результат даст объявление List ManList; и т. д. Вспомните, что работать с объектами производных классов можно с помощью указателей на базовый класс.

    Шаблоны

    Шаблоны
    STL — это библиотека шаблонов. Прежде всего вспомним, что такое шаблон. Различают шаблоны функций и шаблоны классов. Шаблон функций (function template) является средством языка C++, позволяющим избежать рутинного переписывания кодов функций, которые имеют сходный алгоритм, но разные типы параметров. Классическим примером, иллюстрирующим выгоды шаблона, является множество реализаций функции max (a, b) . При отсутствии механизма шаблонов для придания функции max () универсального характера следует создать несколько функций, разделяющих одно и то же имя. Например:

    long max (long a, long b);

    double max (double a, double b);

    MyType max (mytype a, mytype b);

    Vectors max (Vectors a, Vectors b);

    Очевидно, что тела всех функций могут выглядеть совершенно одинаково для многих типов параметров. Например, коды функции max могут иметь вид:

    return (a>b) ? а : b;

    В таких случаях удобно использовать шаблон функции. Шаблон задается ключевым словом template:

    template Т max(Т х, Т у)
    {

    return (х>у) ? х : у;
    };

    Описатель — это аргумент шаблона. Символ т (type) означает произвольный тип данных т, который будет задан при использовании шаблона, т выполняет роль формального параметра, поэтому сам символ (т) может быть и другим, но везде одинаковым. При фактическом использовании шаблона место т заменяет какой-то уже описанный тип. Им может быть как стандартный, встроенный тип языка, так и новый тип, определенный пользователем. В том числе он может быть именем класса, определенного ранее. Важно, чтобы для типа был определен смысл операции > (больше). Если т заменяется классом, то в классе должна быть предварительно реализована операция operator> ().

    Примечание
    Примечание

    Не идите на поводу у ложного друга — переводчика термина operator. В английском языке он имеет смысл операции (например, операция + или операция <, операция логического или |, и т. д.). То, что мы называем оператором языка (например, оператор while, оператор for, условный оператор if, и т. д.), имеет английский аналог — statement (например, conditional statement if).
    Если задан шаблон, то компилятор генерирует подходящие коды функции max () в соответствии с конкретными типами фактических параметров, использованных при вызове функции. Например, встретив во внешней функции коды:

    Man a("Alex Black", 54), b("Galina Black", 44), с;

    с = max (a, b);

    cout « "\n Старший: " « с;

    компилятор в сгенерированной по шаблону копии функции max при сравнении объектов класса Man использует функцию operator > (), которая должна быть определена внутри класса Man. Например, так:

    int operator >(Man& m) { return m__Age > m. m_Age; }

    Если в той же внешней функции встретится оператор:

    cout « "\n max (10,011) = " « max (10,011);

    то компилятор в другой копии функции max, сгенерированной по тому же шаблону, использует операцию >, определенную для стандартного типа данных int. Один раз написав шаблон функции max, мы можем вызывать ее для всех типов данных, для которых определена операция operator> (). Если для какого-то типа данных тело функции max не годится, то можно отменить (override) действие шаблона функции для этого типа. Например, определив функцию:

    char* max (char* s, char *t)
    {

    return (strcmp (s, t) >0) ?s : t;
    }

    мы отменяем действие шаблона для символьных строк, так как функция, скроенная по шаблону, осуществляла бы ничего не значащее сравнение указателей s и t. При использовании шаблона следует строго соблюдать типы параметров и не надеяться на стандартные преобразования типов, по умолчанию осуществляемые компилятором при вызове обычных функций. Например, явно заданную функцию, скрывающую (отменяющую) шаблон:

    double max (double, double);

    можно вызывать с аргументами (int, double) или (float, long). Компилятор при этом автоматически преобразует параметры к типу double. Однако если явная декларация функции, скрывающей шаблон, отсутствует, то шаблон

    template T max(Т х, Т у)

    не позволит смешивать типы при вызове функции max. Таким образом, обращение int i=max (9, 8.); вызывает сообщение об ошибке: "Could not find a match for max (int, double) ", которое означает, что не найдена функция max () для пары аргументов типа (int, double).

    Стек — это несложно

    Стек — это несложно
    Стек — это адаптер (container adaptor), который предоставляет ограниченное подмножество всей функциональности контейнера. Термин адаптер в применении к структуре данных STL означает, что она реализована на основе какой-то другой структуры. По умолчанию стек основан на контейнере типа deque, но при объявлении можно явно указать и другой тип контейнера. Стек поддерживает вставку, удаление и инспекцию элемента, расположенного в первой (top) позиции контейнера. Стек не допускает итераций прохода по своим элементам. Говорят, что стек является структурой данных с дисциплиной доступа "last in first out" (LIFO). Вверху стека расположен элемент, который был помещен в него последним. Только он и может быть выбран в настоящий момент. При отладке следующего фрагмента не забудьте вставить директиву #include :

    void main()
    {

    //========= Создаем стек целых

    stack s;

    s.push(joy);

    s.push(joe);

    s.push(charlie);

    //========= Проверяем очевидные вещи

    assert (s.size () == 3);
    assert(s.top() == Charlie);

    cout « "Stack contents:\n\n";

    while (s.size())

    {

    cout « s.top() « "; ";

    //========= Уничтожает top-элемент

    s.pop(); }

    assert(s.empty());
    }

    Связыватели и адаптеры

    Связыватели и адаптеры

    * Связывателями (binders) называются вспомогательные шаблоны функций, которые создают некий объект (adaptor) , подстраивающий или преобразующий бинарный функциональный объект в унарный путем привязывания недостающего аргумента. Звучит запутанно, но суть достаточно проста. Представьте, что надо найти в нашей последовательности людей первого человека, который моложе, чем

    Man win("Winton Kelly", 50);

    Для объектов класса Man уже определена бинарная операция operator< (), которой пользуется предикат less (), и мы показали использование этого предиката в алгоритме сортировки по возрасту. В том примере функция sort сама подставляла оба параметра в бинарную функцию operator< (), сравнивая объекты для нужд сортировки. Теперь мы используем связыватель bind2nd, для того чтобы зафиксировать (привязать) второй параметр этой функции и сделать его равным объекту win. Первый параметр при этом остается свободным, он будет пробегать по всему контейнеру. Таким образом, мы сможем сравнить все объекты последовательности с одним фиксированным объектом win.

    В этом же фрагменте мы покажем, как использовать другой адаптер mem_f un_ref, который тоже является вспомогательным шаблоном функции для вызова какой-либо функции, являющейся членом класса, в нашем случае Man. Вызов осуществляется для всех объектов класса в процессе прохода по контейнеру. Введите в состав класса Man две public-функции, выделяющие имя и фамилию человека. В коде этих функций попутно демонстрируются методы класса string, которые позволяют осуществлять поиск и выделение подстроки:

    //======== Выделяем имя

    Man FirstName()
    {

    //======== Ищем первое вхождение пробела

    int pos = m_Name.find_first_of(string(" "),0);

    string name = m_Name.substr(0, pos);

    cout « '\n' « name;

    return *this;
    }

    //======== Выделяем фамилию

    Man SurName()
    {

    //======== Ищем последнее вхождение пробела

    int pos = m_Name.find_last_of(" "), num = m_Name.length () - pos;

    string name = m_Name.substr(pos + 1, num);

    cout « '\n' « name; return *this;
    }

    Вектор заполняется элементами, взятыми из массива а г, и при этом используется метод assign, который стирает весь массив и вновь заполняет его элементами, копируя их из диапазона памяти, задаваемого параметрами. Далее мы показываем, как используется связыватель bind2nd и адаптер члена-функции mem_f un_ref:

    void main ()
    {

    Man ar[] =
    {

    joy, joe, zoran, тагу, simon, liza, Man("Lina Groves", 19)
    };

    uint size = sizeof(ar)/sizeof(Man);

    vector men;

    men.assign(ar, ar+size);

    pr(men,"Man Vector");

    //======= Привязка второго аргумента

    vector::iterator p = find_if(men.begin(),

    men.end(), bind2nd(less(), win));

    if (p != men.end())

    cout « "\nFound a man less than " « win « "\n\t" « *p;

    //======= Использование метода класса (mem_fun_ref)

    cout « "\n\nMen Names:\n";

    for_each (men.begin(), men.end(), mem_fun_ref(&Man::SurName));

    cout « "\n\nMen First Names:\n";

    for_each (men.begin (), men.end(), mem_fun_ref(&Man::FirstName));
    cout « "\n\n";
    }

    Напомним, что для успешной работы вы должны вставить в функцию main тот набор объектов класса Man, который был приведен ранее.

    Примечание
    Примечание

    При анализе этого кода бросается в глаза неестественность прототипов функций SurName и FirstName. Логика использования этих функций совсем не требует возвращать какое-либо значение, будь то Man, или переменная любого другого типа. Естественным выбором будет прототип void SurNameQ;. Но, к сожалению, этот выбор не проходит по неизвестным мне причинам ни в Visual Studio б, ни в Studio.Net 7.O. Я достаточно много времени потратил на бесполезные поиски ответа на этот вопрос и пришел к выводу, что это ошибка разработчиков. В подтверждение такого вывода приведу следующие аргументы. Во-первых, измените тип возвращаемого значения на любой другой, но не void, и программа будет работать. Например, возьмите прототип string SurName(); и возвращайте return "MicrosoftisOK"; (или другую пару: int и-127). Во-вторых, все примеры на (mem_fun_ref) в документации MSDN возвращают загадочный bool. В-третьих, в документации SGI (Silicon Graphics) приведены аналогичные примеры с функциями, возвращающими void. Там, как вы знаете, используется другая платформа (IRIS). В-четвертых, наш пример (без void) проходит в Visual Studio б и не работает в бета-версии Studio.Net. Будем надеяться, что ситуация со временем выправится.

    Адаптер mem_fun в отличие от mem_fun__ref используется с контейнерами, хранящими указатели на объекты, а не сами объекты. Хорошим примером использования mem_f un, в котором иллюстрируется полиморфизм позднего связывания (late binding polymorphism), является следующий:

    //======== Базовый класс. К сожалению, абстрактным

    //======= его не позволит сделать контейнер

    struct Stud

    virtual bool print()
    {

    cout « "\nl'm a Stud";

    return true;

    }
    };

    //===== Производный класс struct GoodStud : public Stud

    {
    bool print ()
    {

    cout « "\nl'm a Good Stud";
    return true;
    }
    };

    //======= Еще один производный класс

    struct BadStud : public Stud
    {

    bool print ()
    {

    cout « "XnI'm a Bad Stud";
    return true;
    }
    };

    //======= Иллюстрируем полиморфизм в действии

    void main () {

    //====== Вектор указателей типа Stud*

    vector v;

    //====== Они могут указывать и на детей

    v.push_back (new StudO);

    v.push_back (new GoodStudO);

    v.push_back(new BadStud(J);

    //====== Выбор тела метода происходит поздно

    //====== на этапе выполнения

    for_each(v.begin(), v.end(), mem_fun(&Stud:: print));
    cout <<"\n\n";
    }

    Конечно же, эта программа выведет:

    I'm a Stud

    I'm a Good Stud

    I'm a Bad Stud

    так как mem_fun будет вызвана с помощью указателя типа stud* (на базовый класс) — непременное условие проявления полиморфизма, то есть выбора конкретной версии виртуальной функции (адреса функции из vtable) на этапе выполнения. Выбор определяется конкретной ситуацией — типом объекта, попавшим под родительский перст (указатель) в данный момент времени.

    Cамоучитель по Visual Studio.Net

    Диалог для исследования решений

    Диалог для исследования решений

    Сейчас мы быстрыми темпами, не углубляясь в детали, создадим диалог, работающий в немодальном режиме и позволяющий исследовать решения уравнения Пуассона при разных значениях свойств среды, произвольном расположении источников поля и произвольных граничных условиях.

    Так как диалог будет вызываться по команде меню, откройте в окне редактора ресурс меню IDR_MAINFRAME и приведите его в соответствие со следующей схемой. В меню File должна быть только одна команда Exit, в меню Edit уберите все команды и вставьте одну команду Parameters, индекс (ID_EDIT_PARAMETERS) ей будет присвоен автоматически. Остальные меню оставьте без изменения. С помощью редактора диалогов создайте новое диалоговое окно (форму), которое имеет вид, изображенный на Рисунок 11.4. Типы элементов управления, размещенных в окне диалога, и их идентификаторы сведены в табл. 11.1. Затем создайте класс для управления диалогом.

  • Вызовите контекстное меню в форме диалога и выберите команду Add Class.
  • В качестве типа класса выберите MFC Class.
  • В окне мастера MFC Class Wizard задайте имя класса CParamDlg, базовый класс CDialog, идентификатор диалога: IDD_PARAM и нажмите кнопку Finish.


  • Форма диалога для управления параметрами краевой задачи

    Рисунок 11.4. Форма диалога для управления параметрами краевой задачи

    Форма диалога для управления параметрами краевой задачи

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

    Элемент

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

    Диалог

    IDD_PARAM

    Окно редактирования Source

    IDC_SOURCE

    Окно редактирования Start группы Source

    IDC_SOURCE1

    Окно редактирования End группы Source

    IDC_SOURCE2

    Окно редактирования Value

    IDC_PROP

    Окно редактирования Start группы Properties

    IDCLPROP1

    Окно редактирования End группы Properties

    IDC_PROP2

    Окно редактирования Nodes

    IDC.NODES

    Окно редактирования Distance

    IDCJHST

    Окно редактирования Decrement

    IDC_DECR

    Окно редактирования g группы Left Boundary

    IDC_LEFTG

    Окно редактирования d группы Left Boundary

    IDCJ.EFTD

    Окно редактирования g группы Right Boundary

    IDC_RIGHTG

    Окно редактирования d группы Right Boundary

    IDC_RIGHTD

    Кнопка Add группы Source

    IDC_ADDSOURCE

    Кнопка Add группы Properties

    IDC_ADDPROP

    Кнопка Apply

    IDC_APPLY

    Кнопка Close

    IDCANCEL

    Вручную введите изменения в файл с объявлением класса, так чтобы он стал: ftpragma once

    class CParamDlg : public CDialog {

    //===== Будем общаться с окном

    friend class CChildView;

    DECLARE_DYNAMIC(CParamDlg)

    public:

    //===== Будем помнить его адрес

    CChildView *m_pView;

    //===== В конструкторе запросим его адрес

    CParamDlg(CChildView* р) ;

    virtual ~CParamDlg () ;

    // Dialog Data

    enum { IDD = IDD_PARAM );

    protected:

    virtual void DoDataExchange(CDataExchange* pDX) ;
    DECLARE_MESSAGE_MAP() );

    Для всех четырех кнопок на форме диалога создайте обработчики уведомлений, или, используя терминологию Microsoft, Control Event типа BN_CLICKED. Вы помните, что это делается с помощью небольшой кнопки Control Event, которая расположена на панели инструментов окна Properties. В это окно надо входить в тот момент, когда фокус находится на соответствующей кнопке. Во всяком случае, именно так это работает в бета-версии Studio.Net.

    Для обмена данными с шестью окнами редактирования (IDC_SOL)RCE, IDC_SOURCE1, IDC_SOURCE2, IDC_PROP, IDC_PROP1, IDC_PROP2) создайте с помощью мастера Add Member Variable Wizard шесть переменных:

    //==== Интенсивность источника поля

    double m_Source;

    // Индекс ячейки сетки, где расположено начало источника

    int m_Src!dl;

    // Индекс ячейки сетки, где расположен конец источника

    int m_Srdd2;

    //==== Значение физического свойства ячейки сетки
    double m_Prop;

    // Индексы начала и конца области со свойством
    m_Prop int m_PropIdl;
    int m_PropId2;

    В результате этих действий в классе CParamDlg кроме указанных переменных должны появиться шесть вызовов функции обмена данными DDX_Text, которые мастер размещает внутри функции CParamDlg::DoDataExchange. Вручную добавьте в DoDataExchange еще семь вызовов функции DDX_Text для обмена данными с переменными, которые расположены не в диалоговом, а в оконном классе (cchildview). После этого функция должна приобрести вид:

    void CParamDlg::DoDataExchange(CDataExchange* pDX) {

    DDX_Text (pDX, IDC_PROP2, m_Prop!d2);

    DDXJText(pDX, IDC_PROP1, m_Prop!dl);

    DDX_Text(pDX, IDC_PROP, m_Prop);

    DDX_Text(pDX, IDC_SOURCE2, m_Srdd2);

    DDX_Text(pDX, IDC_SOURCE1, ra_SrcIdl);

    DDX_Text(pDX, IDC_SOURCE, m_Source);

    //===== Обмен с переменными оконного класса
    DDX_Text(pDX, IDC_NODES,m_pView->m__n);
    DDX_Text(pDX, IDC_DIST, m_pView->m_L);
    DDX_Text(pDX, IDC_DECR, m_pView->m_k);
    DDX_Text(pDX, IDC_LEFTG, m_pView->m_g0);
    DDX_Text(pDX, IDC_LEFTD, ra_pView->m_d0);
    DDX_Text(pDX, IDC_RIGHTG, mj?View->m_gn);
    DDX_Text(pDX, IDC_RIGHTD, m_pView->m_dn);
    CDialog::DoDataExchange(pDX);
    }

    При нажатии на одну из кнопок Add в соответствующем контейнере параметров системы (m_f или m_r) должны произойти замены значений по индексам, определяемым диапазоном (m_Srddl, m_Srdd2) ИЛИ (m_Prop!dl, m_Prop!d2). В первом случае вы вводите новые источники поля, а во втором — изменяете свойства среды. В уже существующие заготовки функций обработки нажатия на кнопки введите такие коды:

    void CParamDlg::OnClickedApply(void) {

    //====== Считываем данные из окон

    UpdateDataO ;

    //====== Заново решаем систему и выводим график

    m_jpView->Solve () ; }

    void CParamDlg::OnClickedAddsource(void)
    {
    UpdateData();

    //====== Изменяем контейнер m_f (источников поля)

    for (int i=m_Src!dl; i <= m_Srdd2; i + + ) {

    if (0 <= i && i < m_pView~>m_n)

    m_pView->m_f[i] = -m_Source; )

    m_pView->Solve0; }

    void CParamDlg::OnClickedAddprop(void) { UpdateDataO ;

    //====== Изменяем контейнер m_r (свойств среды)

    for (int i=m_Prop!dl; i <= m_PropId2; i++) {

    if (0 <= i &i i < m_pView->m_n && m_Prop > 0.)

    m_pView->ra_r[i] = m_Prop; }

    m_pView->Solve(); }

    void CParamDlg::OnClickedCancel(void)
    {

    //====== Закрываем немодальный диалог

    m_pView->m_pDlg = 0;

    DestroyWindow(); }

    Измените коды конструктора класса так, чтобы запоминался обратный указатель на объект оконного класса. Заодно сверьте начало файла ParamDlg.h с тем фрагментом, что приведен ниже:

    #include "stdafx.h"

    #include "Heat.h"

    #include"ParamDlg.h"

    IMPLEMENT_DYNAMIC(CParamDlg, CDialog)

    CParamDlg::CParamDlg(CChildView* p)

    : CDialog(CParamDlg::IDD, p)
    {

    m_pView = p;

    //===== Начальное значение свойств среды

    //===== не должно равняться нулю

    m_Prop =1.0;

    m_Prop!dl = 0;

    m_Prop!d2 = 0;

    m_Source =0.0;

    m_Src!dl = 0;

    m_Srdd2 = 0;
    }

    CParamDlg::~CParamDlg()

    {

    }

    Инициализация диалога, как вы помните, должна производиться в обработчике сообщения WM_INITDIALOG. Здесь я опять попадаю в ловушку. В рамках Visual C++ Studio.Net вы не найдете WM_INITDIALOG в списке доступных сообщений, но вместо этого найдете функцию OnlnitDialog в списке виртуальных функций (overrides). Введите в класс CParamDlg эту функцию. В ней мы просто отодвинем окно диалога, чтобы оно при появлении на экране не заслоняло график. Другие установки должны происходить автоматически:

    BOOL CParamDlg::OnInitDialog(void) {

    CDialog:rOnlnitDialog();

    CRect r;

    //===== С помощью контекста устройства
    //===== узнаем размеры всего экрана CClientDC dc(this);

    int w = dc.GetDeviceCaps(HORZRES);
    int h = dc.GetDeviceCaps(VERTRES);

    //===== Узнаем размеры окна диалога GetWindowRect(&r);

    //===== Смещаем его вправо и вниз
    r.OffsetRect(w-r.right-10,h-r.bottom-30);
    MoveWindow(Sr);
    return TRUE;
    }

    В данный момент полезно запустить приложение и поучиться сражаться с ошибками, которые вызваны тем, что классы приложения не очень хорошо знакомы между собой. Добавьте директиву:

    #include "ChildView.h"

    в список директив файла ParamDlg.cpp, а также директиву

    #include "ParamDlg.h"

    в список директив файла ChildView.cpp. После этого исправления вы увидите еще одно сообщение об ошибке, которое напомнит вам о том, что еще не реализована работа с диалогом в немодальном режиме. Для этого надо немного потрудиться. Введите в класс CChildView реакцию на событие выбора пользователем команды меню ID_EDIT_PARAMETERS. Напомним, что это делается с помощью кнопки Events окна Properties. В обработчике мы открываем диалог в немодальном режиме:

    void CChildView::OnEditParameters(void) {

    //===== Если диалог не открыт,

    if (!m_pDlg)

    {

    //== Динамически создаем объект диалогового класса

    m_pDlg = new CParamDlg(this);

    //== и после этого создаем окно диалога

    m_pDlg->Create(IDD_PARAM);
    }
    }

    В окне свойств для формы диалога установим в True свойство visible. В классе cParamDlg следует переопределить виртуальную функцию PostNcDestroy, в теле которой необходимо освободить память, занимаемую объектом диалогового класса:

    void CParamDlg::PostNcDestroy(void)
    {
    delete this;

    }

    После этого диалог должен работать. Задайте точечный источник поля в узле 100, и вы увидите график решения, которое имеет вид, показанный на Рисунок 11.5.



    Учитывая сказанное, создадим программный модуль,

    Формирование матрицы

    Учитывая сказанное, создадим программный модуль, который позволяет проверить наши возможности управления последовательностью valarray на примере задачи, близкой к реальности. Самым сложным моментом в реализуемом плане является создание функции f (), с помощью которой генерируется матрица заданной структуры, но произвольной размерности п. При генерации она помещается в последовательность типа valarray. Вторая функция (f и) проста. С ее помощью вычисляются коэффициенты уже известного вектора решений1:

    //====== Глобально заданная размерность системы

    int n;

    //====== Граничные условия

    double UO, UN;

    //====== Функция вычисления коэффициентов

    //====== трехдиагональной матрицы

    double f ()
    {

    //====== Разовые начальные установки

    static int raw = -1, k = -1, col = 0;

    //====== Сдвигаемся по столбцам

    col++;

    //====== k считает все элементы

    //====== Если начинается новая строка

    if (++k % n == 0)
    {

    col =0; // Обнуляем столбец

    raw++; // Сдвигаемся по строкам
    }

    //====== Выделяем три диагонали

    return col==raw ? -2.

    : col == raw-1 И col==raw+l ? 1.

    : 0.;

    }

    double fu()

    {

    //==== Вычисления вектора правых частей по формуле (5)
    static double

    dU = (UN-UO)/(n+1),

    d = U0; return d += dU;
    }

    В функции main создается valarray с трехдиагональной матрицей и производится умножение матрицы на вектор решений. Алгоритм умножения использует сечение, которое вырезает из valarray текущую строку матрицы:

    void main()
    {

    //======= Размерность задачи и граничные условия

    n =4;

    UO = 100.;

    UN = 0 . ;

    //======= Размерность valarray (вся матрица)

    int nn = n*n;

    //======= Матрица и два вектора

    valarray a(nn), u(n), v(n);

    //======= Генерируем их значения

    generate (&а[0], &a[nn], f); generate (&u[0], &u[n], fu);

    out ("Initial matrix", a); out ("Initial vector", u);

    //======= Умножение матрицы на вектор

    for (int i=0; i
    //======= Выбираем i-ю строку матрицы

    valarray s = a[slice (i*n, n , 1)];

    //======= Умножаем на вектор решений

    //======= Ответ помещаем в вектор v <

    transform(&s[0], &s[n], &u[0], &v[0], multiplies());

    //======= Суммируем вектор, получая

    //======= i-ый компонент вектора правых частей

    cout « "\nb[" « i « "] = " « v.sum(); }

    cout«"\n\n";
    }

    Так как мы знаем структуру вектора правых частей, то, изменяя граничные условия и порядок системы, можем проверить достигнутую технику работы с сечениями.

    Тестирование обнаруживает появление численных погрешностей (в пределах Ю"15), обусловленных ограниченностью разрядной сетки, в случаях когда диапазон изменения искомой величины не кратен размеру расчетной области. Стоит отметить, что сечения хоть и являются непривычным инструментом, для которого хочется найти наилучшее применение, но в рамках нашей задачи вполне можно обойтись и без него. Например, алгоритм умножения матрицы на вектор можно выполнить таким образом:

    for (int i=0, id=0; i {

    transform(&a[id], &a[id+n], &u[0], &v[0],

    multiplies () ) ;

    cout « "\nb[" « i « "] = " « v.sum();
    }

    Эффективность этой реализации, несомненно, выше, так как здесь на один valarray меньше. Я думаю, что вы, дорогой читатель, сами найдете достойное применение сечениям.

    Класс графика

    Класс графика
    С помощью Studio.Net введите в состав проекта новый generic-класс CGraph, не указывая имени базового класса и не включая флажок Virtual destructor. В файл декларации нового класса введите вручную вспомогательный класс CDPoint, необходимость в котором мы обсуждали ранее. Затем добавьте объявление структуры TData, которая собирает воедино все данные, используемые при построении графика. Начальная буква Т в имени класса осталась со времен работы в среде Borland. Там принято все классы именовать начиная с буквы Т (Туре), означающей создание нового типа данных. Но в отличие от старой реализации графика, которая, возможно, знакома читателю по книге «Технологии программирования на языке C++» (Издательство СПбГТУ, 1997), мы введем в класс CGraph некоторые новые возможности:

    #pragma once

    class CDPoint

    {

    public:

    //=== Две вещественные координаты точки на плоскости

    double x, у;

    //======= Стандартный набор конструкторов и операций

    CDPoint () {

    х=0.; у=0.;
    }

    CDPoint(double xx, double yy) {

    х=хх;
    У=УУ;
    }

    CDPoints operator=(const CDPointi pt) {

    x = pt.x;

    У = pt.y; return *this;

    }

    CDPoint(const CDPointS pt) {

    *this - pt; } };

    //===== Вспомогательные данные, характеризующие
    //== последовательность координат вдоль одной из осей
    struct TData (

    //===== Порядок в нормализованном представлении числа
    int Power; //===== Флаг оси X
    bool bХ; double

    //======= Экстремумы

    Min, Max,

    //======= Множитель -(10 в степени Power)
    {

    Factor,

    //======= Шаг вдоль оси (мантисса)

    Step,

    //======= Реальный шаг

    dStep,

    //==== Первая и последняя координаты (мантиссы)

    Start, End,

    // ======= Первая и последняя координаты

    dStart, dEnd; };

    //===== Класс, реализующий функции плоского графика
    class CGraph { public:

    //===== Данные, характеризующие данные вдоль осей

    TData m_DataX, m_DataY;

    //===== Контейнер точек графика

    vector & m_Points;

    //===== Текущие размеры окна графика

    CSize m_Size;

    //===== Экранные координаты центра окна

    CPoint m_Center;

    //===== Заголовок и наименования осей

    CString m_sTitle, m_sX, m_sY;

    //===== Перо для рисования

    CPen m_Pen;

    //===== Два типа шрифтов

    CFont m_TitleFont, m_Font;

    //===== Высота буквы (зависит от шрифта)

    int m_LH,

    //===== Толщина пера

    m_Width;

    //===== Цвет пера COLORREF m_Clr;

    //======= Методы для управления графиком

    CGraph(vector& pt, CString sTitle, CString sX, CString sY) ;

    virtual -CGraph();

    //===== Заполнение TData для любой из осей

    void Scale(TDataS data);

    //===== Переход к логическим координатам точек

    int MapToLogX (double d);
    int MapToLogY (double d);
    //===== Изображение в заданном контексте

    void Draw (CDC *pDC);

    //===== Изображение одной линии

    void DrawLine(CDC *pDC) ;

    //===== Подготовка цифровой метки на оси

    CString MakeLabel(bool bx, doubles d);
    };

    Класс CGraph сделан с учетом возможности развития его функциональности, так чтобы вы могли добавить в него нечто и он мог бы справиться с несколькими кривыми одновременно. Фактически он представляет собой упрощенную версию того класса, которым мы пользуемся для отображения результатов расчета поля в двухмерной постановке. Отметьте, что структура TData используется как для последовательности абсцисс, так и ординат.

    Алгоритм нормирования абсцисс и ординат проще создать, чем кратко и понятно описать. Тем не менее попробуем дать ключ к тому, что происходит. Мы хотим, чтобы размеры графика отслеживали размеры окна, а числа, используемые для разметки осей, из любого разумного диапазона, как можно дольше оставались читабельными. Задача трудновыполнимая, если динамически не изменять шрифт. В данной реализации мы не будем подбирать, а используем только два фиксированных шрифта: для оцифровки осей и для вывода заголовка графика. Обычно при построении графиков числа, используемые для оцифровки осей (мантиссы), укладываются в некоторый разумный диапазон и принадлежат множеству чисел, кратных по модулю 10, стандартным значениям шага мантиссы (2, 2.5, 5 и 10). Операцию выбора шага сетки, удовлетворяющую этим условиям, удобно выполнить в глобально определенной функции, не принадлежащей классу CGraph. Это дает возможность использовать функцию для нужд других алгоритмов и классов. Ниже приведена функция gScale, которая выполняет подбор шага сетки. Мы постепенно дадим содержимое всего файла Graph.срр, поэтому вы можете полностью убрать существующие коды заготовки. Начало файла имеет такой вид:

    #include"StdAfx.h"

    #include "graph.h"

    //===== Доля окна, занимаемая графиком

    #define SCAT,F,_X 0 . 6

    #define SCALE_Y 0.6

    //=== Внешняя функция нормировки мантисс шагов сетки

    void gScale (double span, doubles step)

    {

    //== Переменная span определяет диапазон изменения

    //== значаний одной из координат точек графика

    //== Вычисляем порядок числа, описывающего диапазон

    int power = int(floor(loglO(span)));

    //===== Множитель (zoom factor)

    double factor = pow(10, power);

    //===== Мантисса диапазона (теперь 1 < span < 10)

    span /= factor;

    //===== Выбираем стандартный шаг сетки if (span<1.99)

    step=.2;
    else if (span<2.49)

    step=.25;
    else if (span<4.99)

    step=.5;
    else if (span<10.)

    step= 1.;

    //===== Возвращаем реальный шаг сетки (step*10~power)
    step *= factor;
    }

    Результатом работы функции gScale является значение мантиссы дискретного шага сетки, которая наносится на график и оцифровывает оду из осей. Самым сложным местом в алгоритме разметки осей является метод CGraph:: Scale. Он по очереди работает для обеих осей и поэтому использует параметр с данными типа TData, описывающими конкретную ось. Особенностью алгоритма является реализация идеи, принадлежащей доценту СПбГТУ Александру Калимову и заключающейся в том, чтобы как можно дольше не переходить к экспоненциальной форме записи чисел. Обычно Калимов использует форму с фиксированной запятой в диапазоне 7 порядков изменения чисел (10~3+104), и это дает максимально удобный для восприятия формат, повышая читабельность графика:

    void CGraph::Scale (TDatai data)
    {

    //===== С пустой последовательностью не работаем

    if (m_Points.empty()) return;

    //===== Готовимся искать экстремумы

    data.Max = data.bX ? m_Points [0] .х : m_Points [0] .у;

    data.Min = data.Max;

    //===== Поиск экстремумов

    for (UINT j=0; j
    {

    double d = data.bX ?
    m_Points [ j] .x

    : m_Points [ j] . y;
    if (d < data.Min) data.Min = d;
    if (d > data.Max) data.Max = d;
    }

    //===== Максимальная амплитуда двух экстремумов
    double ext = max(fabs(data.Min),fabs(data.Max));

    //===== Искусственно увеличиваем порядок экстремума
    //===== на 3 единицы, так как мы хотим покрыть 7 порядков,
    //===== не переходя к экспоненцеальной форме чисел
    double power = ext > 0.? loglO(ext) +3. : 0.;

    data.Power = int(floor(power/7.));

    //===== Если число не укладывается в этот диапазон
    if (data.Power != 0)

    //===== то мы восстанавливаем значение порядка

    data.Power = int(floor(power)) - 3;

    //===== Реальный множитель

    data.Factor = pow(10,data.Power);

    //===== Диапазон изменения мантиссы

    double span = (data.Max - data.Min)/data.Factor;

    //===== Если он нулевой, if (span == 0.)

    span = 0.5; // то искусственно раздвигаем график

    // Подбираем стандартный шаг для координатной сетки
    gScale (span, data.Step);

    //===== Шаг с учетом искусственных преобразований
    data.dStep = data.Step * data.Factor;

    //== Начальная линия сетки должна быть кратна шагу
    //====и быть меньше минимума
    data.dStart = data.dStep *

    int (floor(data.Min/data.dStep));
    data.Start = data.dStart/data.Factor;

    //===== Вычисляем последнюю линию сетки
    for (data.End = data.Start;

    data.End < data.Min/data.Factor + span-le-10;

    data.End += data.Step)

    data.dEnd = data.End*data.Factor;
    }

    Класс окна для отображения графика

    Класс окна для отображения графика

    Откройте файл ChildView.cpp, который содержит коды реализации методов класса CChildView. Его имя содержит ложный намек на происхождение от CView. На самом деле он происходит от класса CWnd и инкапсулирует функциональность окна, оккупирующего клиентскую область окна рамки, которое управляется классом CMainFrame. Простое окно, как вы помните, для перерисовки своего содержимого, вместо метода OnDraw использует метод OnPaint. Найдите этот метод в классе CChildView и убедитесь, что в нем контекст устройства создается, а не приходит в качестве параметра от каркаса приложения, как это было в приложениях, поддерживающих архитектуру документ — представление. Вставьте внутрь этого метода вызов конструктора класса CGraph с последующим сообщением Draw:

    void CChildView::OnPaint() {

    CPaintDC dc(this);

    CGraph(m_Points, "Field Distribution", "x[m]","Field").Draw(&dc); }

    Класс CGraph разработаем позже. Он будет создавать двухмерный график функции — решения краевой задачи, автоматически масштабируемый и подстраивающийся под текущий размер окна CChildView. Перейдите к файлу с определением оконного класса (ChildFrame.h) и введите следующие коррективы:

    # pragma once

    #include "Graph.h"

    Class CChildView : public CWnd
    {

    // Вспомогательные классы будут пользоваться данными

    friend class CParamDlg;

    friend class CGraph;

    private:

    //===== Контейнер координат точек графика

    vector m_Points;

    //===== Вектор источников и свойств среды (см. f и р)

    vector m_f, m_r;

    //===== Размерность задачи (см. N)

    int m_n;

    //===== Параметры

    double m_k, // Коэффициент k

    m_L, // Протяженность расчетной области

    m_g0, // Коэффициенты, задающие ГУ слева

    m_d0,

    m_gn, // Коэффициенты, задающие ГУ справа m_dn ;
    CParamDlg *m_pDlg; // Немодальный диалог параметров

    public:

    CChildView();

    virtual -CChildViewO;

    virtual BOOL PreCreateWindow(CREATESTRUCT& cs);

    //===== Изменение размерности задачи
    void Resize();

    //===== Решение системы методом прогонки
    void Solve();

    protected:

    afx_msg void OnPaint();

    DECLARE_MESSAGE_MAP() };

    Точки графика будем хранить в контейнере объектов класса CDPoint, который мы неоднократно использовали, так как исследователи реальных систем работают с вещественными координатами. Переход к экранным координатам будет произведен в классе CGraph. Инициализацию данных проведите в конструкторе оконного класса:

    CChildView: :CChildView()
    {

    m_n = 200;

    m_k = -0.0005;

    m_L = 200.;

    //====== Слева ГУ первого рода Uo=100

    m_g0 = 0.;

    m_d0 =100.;

    m_gn = 0.;

    m_dn = 0.;

    Resize () ;

    m_pDlg = 0;
    }

    В деструктор вставьте коды освобождения памяти, занимаемой контейнерами:

    CChildView::~CChildView()
    {

    m_Points.clear();

    m_f.clear();

    m_r.clear();
    }

    При работе с диалогом по управлению параметрами пользователь будет иметь возможность изменять количество узлов разбиения расчетной области, поэтому мы должны изменять размерность используемых контейнеров:

    void CChildView::Resize ()
    {
    //===== Число узлов равно N+1 (с учетом 0-го узла)

    int n = m n + 1;

    m_Points.resize(n, CDPoint(0.,0.));
    m_f.resize(n, 0.);
    m_r.resize(n, 1.); }

    Функция Solve решает систему уравнений методом прогонки:

    void CChildView::Solve()
    {

    Resize () ;

    int n = m_n + 1;

    //======= Коэффициенты разностных уравнений

    vector a(n), b(n), c(n);

    //======= Коэффициенты прогонки

    vector d(n), e(n);

    double h = m L / m_n, // Размер шага вдоль оси х
    hh = h * h;
    // Квадрат шага

    //======= Коэффициенты с 0-м индексом не используются

    а[0] = 0.;
    b[0] = 0.;
    с[0] = 0.;

    //=== Вычисляем координаты х и коэффициенты уравнений

    m_Points[0].х = 0.;

    for (int i=1; i < m_n; i++)

    {

    m_Points[i],x = i * h;

    //======= Смотри формулы (4)

    a[i] = m_r[i-l]/hh;

    c[i] = m_r[i]/hh;

    b[i] = - a[i] - c[i] + m_k;
    }

    m_Points[m_n].x = m_L;

    //======== -Прямой ходпрогонки

    d[0] = m_gO; //ГУ слева e[0] * m_d0; double den;

    for (i=1; i < m_n; 1++)
    {

    //======= Общий знаменатель

    den = a[i) * d[i-l] + b[i] ; d[i] = -c[i] / den;

    e[i] = }

    //======= Готовимся к обратному ходу

    den = 1. - m_gn * d[m_n-l];

    //======= Случай некорректно поставленной задачи

    if (den==0.)

    {

    MessageBox ("ГУ заданы некорректно", "Ошибка-",МВ_ОК) ;
    return;
    }

    //====== Два последних узла используют ГУ справа
    //======= Смотри формулы (13)

    m_Points[m_n-l].у = (e[m_n-l] + m_dn * d[m_n-l])/den;
    m_Points[m_n].y = (m_dn + m_gn* e[m_n-l])/den;

    //======= Обратный ход прогонки

    for (i = m_n-2; i >= 0; i--)

    m_Points[i].y = d[i) * m_Points[i+1].у + e[i]; Invalidate();

    }

    С помощью инструментов Studio.Net введите в класс CChildView реакцию на сообщение о создании окна WM_CREATE и вставьте в нее единственную строку, которая вызывает функцию Solve. Она формирует и решает систему разностных уравнений, определенную данными по умолчанию. Позже мы создадим диалог по изменению этих данных:

    int CChildView::OnCreate(LPCREATESTRUCT IpCreateStruct)
    {

    if (CWnd::OnCreate(IpCreateStruct) == -1)
    return -1;

    //======= Решаем систему, определенную по умолчанию

    Solved;

    return 0;
    }


    Конструктор CGraph

    Конструктор CGraph

    Если вы поняли, что происходит в методе Scale, то дальнейшие манипуляции с данными графика не вызовут у вас затруднений. Рассмотрим конструктор класса CGraph. В первом параметре по ссылке он получает контейнер с точками графика. Для того чтобы исключить копирование всех точек внешнего контейнера, мы инициализируем в заголовке конструктора свою собственную ссылку m_Points на входной контейнер. Кроме контейнера с точками графика пользователь объектом CGraph должен передать два текста, помечающих оси графика (sX, SY) и текст его заголовка (sTitle). В теле конструктора готовим два объекта типа TData для данных о разметке двух осей, создаем два шрифтовых объекта и инициализируем переменные управления параметрами линии графика. В данной реализации мы убрали диалог по изменению атрибутов пера, который вы можете сделать по своему вкусу.

    Примечание
    Примечание

    Следует отметить, что при изображении нескольких кривых функциональных зависимостей на одном графике необходимо дать пользователю возможность управлять стилем пера для каждой кривой в отдельности. В начале книги мы рассматривали, как это делается. Кроме того, следует учитывать существующие традиции изображения научной графики. Например, можно помечать кривые крестиками, ноликами и т. д. Это делается с помощью цикла прохода по точкам кривой и вывода в определенных местах растровых изображений, маркирующих кривую. Эти возможности ввиду необходимости описывать объекты классов Stingray Objective Grid мы также убрали из данной реализации.

    //======= Конструктор класса CGraph

    CGraph::CGraph (vectorS pt,
    CString sTitle, CString sX, CString sY)

    : m_Points(pt)
    {

    //===== Готовим данные, характеризующие оси координат

    ZeroMemory(Sra_DataX, sizeof(TData));

    ZeroMemory(sm_DataY, sizeof(TData));

    m_DataX.bX = true;

    m_DataY.bX = false;

    m_sTitle = sTitle;

    m_sX = sX;

    m_sY = sY;

    //======= Создаем шрифт для оцифровки осей

    m_Font.CreateFont(16,0,0,0,100,0,0,0,DEFAULT_CHARSET,

    OUT_RASTER_PRECIS,CLIP_DEFAULT_PRECIS,

    DEFAULT_QUALITY,FF_DONTCARE,"Arial");

    //======= Выясняем вертикальный размер буквы

    TEXTMETRIC tm; CClientDC dc(0);
    dc.SelectObject(Sm_Font);
    dc.GetTextMetrics(Stm);
    m_LH = tm.tmHeight;

    //======= Создаем шрифт для вывода заголовка

    m_TitleFont.CreateFont(24,О,О,0,100, 0, 0, 0,

    DEFAULT_CHARSET, OUT_RASTER__PRECIS,

    CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, FF_DONTCARE,

    "Times New Roman");

    //======= Задаем параметры пера

    m_Clr = RGB(0,0,255); m_Width = 2;
    }

    Метод прогонки

    Метод прогонки

    Прогонкой называется модификация метода Гаусса для решения систем линейных алгебраических уравнений с трехдиагональной матрицей. Если матрица системы обладает определенными свойствами, то метод прогонки является численно устойчивым и очень эффективным методом, который позволяет практически мгновенно решать одномерные краевые задачи, одну из которых мы рассмотрели в предыдущем разделе. Большинство корректно поставленных физических задач приводит к системе уравнений с хорошей матрицей, и в этих случаях метод прогонки проявляет слабую чувствительность как к погрешностям задания начальных условий, так и к погрешностям вычислительного характера.
    В предыдущем разделе была сформулирована так называемая первая краевая задача, в которой требуется найти значения функции во внутренних узлах сетки при условии, что на границах области они известны. В теории и на практике рассматриваются задачи с более сложными граничными условиями. Например, когда на одной из границ известна не функция, а ее первая производная — граничное условие второго рода. Имеют место и постановки задач с граничными условиями третьего рода, когда на границе должно выполняться какое-то известное заранее соотношение между функцией и ее первой производной. С точки зрения численной реализации все три типа задач можно описать с помощью соотношений одного и того же вида:

    U0=y0U1+б0, (6)

    Un=ynUn-1+бn, (7)

    Они связывают значения разностных аналогов Ui, непрерывной функции U(x) в двух узлах, прилегающих к левой или правой границе. Так, граничное условие первого рода иUo = с может быть задано с помощью пары параметров: у0= 0, б0 = с, а условие второго рода dU/dx|0= с с помощью другой лары: у0 = 1,бo=ch, где h — это шаг сетки. В нашем приложении будет работать немодальный диалог, который позволит пользователю задавать различные типы граничных условий, изменяя численные значения четырех коэффициентов уo, бo, yn, бn

    Суть метода прогонки заключается в том, что, используя специфику структуры матрицы системы уравнений (наличие трех диагоналей), удается получить рекуррентные формулы для вычисления последовательности коэффициентов прогонки, которые позволяют на обратном ходу вычислить значения функции в узлах сетки. Рассматривая конечно-разностное уравнение для первой тройки узлов:

    b1U1+c1U2=-a1U0,

    видим, что оно совпадает по форме с обобщенным граничным условием (6) и связывает между собой два соседних значения U1, и U2. Перепишем его в виде:

    d1U2+e=U1, (8)

    где d1 и е1вычисляются по известным значениям. Наблюдательный читатель заметит, что это справедливо только для задач первого рода. Чуть позже мы получим общее решение. Теперь мы можем исключить £/, из уравнения для следующей тройки узлов:

    a2U1+b2U2+c2U2=f2,

    подставив значение U1 из уравнения (8). После этой процедуры последнее уравнение также может быть приведено к виду:

    d3U3+e2=U2,

    Подстановки можно продолжать и дальше, но для получения рекуррентного соотношения, достаточно рассмотреть одну из них для произвольного индекса i. Подставив

    di-1Ui+ei-1=Ui-1,

    в уравнение

    aiUi-1+biUi+ciUi+1=fi,
    получим:

    Ui=-[CiUi+1/(aidi-1+bi)]+[fi-ai+1*ei+1/(aidi-1+bi)] (9)
    Это соотношение дает две рекуррентные формулы для коэффициентов:

    di=-Ci/(ai*di-1+bi) (10)

    ei=(fi-ai*ei-1)/(aidi-1+bi) (11)

    Цикл вычисления последовательности коэффициентов в соответствии с этими формулами носит название прямого хода прогонки. Начальные значения коэффициентов определяются предварительно заданным граничным условием (6):

    do=yo, eo=бo,

    Цикл прямого хода повторяется N-1 раз. Последними будут вычислены коэффициенты dN-1 и eN-1, которые связывают функции в двух узлах вблизи правой границы:

    Un-1=dn-1Un+en-1 (12)

    Если на правой границе задано условие первого рода Un = с, то уже можно вычислить Un-1 по формуле (12) и далее продолжать обратный ход прогонки, используя уравнения (9) при I = N - 1,..., 1, 0. Если условие более сложное, то вместе с уравнением (12) надо рассмотреть уравнение (7), определяющее граничное условие на правой границе. Напомним его:

    Un=ynUn-1+бn (7)

    Соотношения (7) и (12) составляют систему из двух уравнений с двумя неизвестными. Используя определители, запишем ее решение.

    Un-1=(en-1+бndn-1)/(1-yndn-1) (13)

    Un=(бn+ynen-1)/(1-yndn-1)

    Таким образом, мы нашли значения в двух узлах, лежащих вблизи правой границы расчетной области. Теперь, используя формулу (9) и уменьшая индекс i от N= 2 до 0, можно вычислить все неизвестные £/.. Этот процесс носит название обратного хода прогонки. Почему-то в голову приходит лозунг нашего времени: «Цели ясны, задачи определены. За работу, товарищи!» Нам осталось всего лишь реализовать описанный алгоритм в виде MFC-приложения.

    Отображение графика

    Отображение графика

    График отображается в такой последовательности. Сначала рисуется ограничивающий прямоугольник (рамка), затем дважды вызывается функция Scale, которая подготавливает данные для разметки осей. После этого выводятся экстремальные значения функции. В этот момент в более сложном случае следует создавать и выводить так называемую легенду графика — информацию о соответствии маркеров и стилей линий определенным кривым. Так как мы изображаем только одну кривую, то эта часть работы сведена к минимуму. Перед тем как отобразить координатную сетку, следует создать и выбрать в контекст другое перо (gridPen). Сама сетка изображается в двух последовательных циклах прохода по диапазонам координат, подготовленных в методе Scale.
    В каждом цикле мы сначала нормируем текущую координату, затем преобразовываем ее в оконную, вызывая одну из функций типа MapToLog*. Одновременно с линией сетки выводится цифровая метка. В ходе процесса нам несколько раз приходится менять способ выравнивания текста (см. вызовы SetTextAlign). Подстройка местоположения текста осуществляется с помощью переменной m_LH (better Height), значение которой зависит от выбранного размера шрифта. После вывода координатной сетки происходит вывод трех строк текста: метки осей и заголовок графика. В последнюю очередь происходит вывод самой кривой графика. В более сложном случае, который не реализован, мы в цикле проходим по всем объектам класса MyLine и просим каждую линию изобразить себя в нашем контексте устройства. Каждая линия при этом помнит и использует свой стиль, толщину, цвет и маркировку:
    void CGraph::Draw(CDC *pDC) {

    //====== С помощью контекста устройства

    //====== узнаем адрес окна, его использующего

    CWnd *pWnd =pDC->GetWindow();

    CRect r;

    pWnd->GetClientRect(ir);

    //====== Уточняем размеры окна

    m_Size = r.Size();

    m_Center = CPoint(m_Size.cx/2, m_Size.cy/2);

    //====== Сохраняем атрибуты контекста

    int nDC = pDC->SaveDC();

    //====== Создаем черное перо для изображения рамки

    CPen pen(PS_SOLID, О, COLORREF(0));
    pDC->SelectObject(Spen);

    //====== Преобразуем координаты рамки

    int It = MapToLogX(-0.S),

    rt = MapToLogX(0.S),

    tp = MapToLogY(0.S),

    bm = MapToLogY(-0.S);
    pDC->Rectangle (It, tp, rt, bm);

    //====== Задаем цвет и выравнивание текста

    pDC->SetTextColor (0);
    pDC->SetTextAlign(TA_LEFT | TA_BASELINE);

    //====== Выбираем шрифт

    pDC->SelectObject (&m_Font);

    //====== Вычисляем атрибуты координатных осей

    Scale(m_DataX); Scale(m_DataY);

    //====== Выводим экстремумы функции

    CString s;

    s.Format("Min = %.3g",m_DataY.Min);

    pDC->TextOut(rt+m_LH, tp+m_LH, s) ;

    s.Format("Max = %.3g",m_DataY.Max);

    pDC->TextOut(rt+m_LH, tp+m_LH+m_LH, s);

    //====== Готовимся изображать координатную сетку

    CPen gridPen(PS_SOLID, 0, RGB(92,200, 178));
    pDC->SelectObject(SgridPen);
    pDC->SetTextAlign(TA_CENTER | TA_BASELINE);

    //======Рисуем вертикальные линии сетки
    for (double x = m_DataX.Start;

    X < m_DataX.End - m_DataX.Step/2.;

    x += m_DataX.Step) {

    //====== Нормируем координату х

    double xn = (x - m_DataX.Start) /

    (m_DataX.End - m_DataX.Start) - 0.5;

    //====== Вычисляем оконную координату

    int xi = MapToLogX(xn);

    //====== Пропускаем крайние линии,

    //====== так как они совпатают с рамкой

    if (x > m_DataX.Start && x < m_DataX.End)
    {

    pDC->MoveTo(xi, bm);

    pDC->LineTo(xi, tp); )

    //====== Наносим цифровую метку

    pDC->TextOut (xi, bm+m_LH, MakeLabel(true, x)); }

    //=== Повторяем цикл для горизонтальных линий сетки
    pDC->SetTextAlign(ТА RIGHT | TA_BASELINE);
    for (double у = m_DataY.Start;

    у < m_DataY.End - m_DataY.Step/2.; у += m_DataY.Step)
    {

    double yn = (y - m_DataY.Start) /

    (m_DataY.End - m_DataY.Start) - 0.5;

    int yi = MapToLogY(yn);

    if (y > m_DataY. Start &S, у < m_DataY.End)
    {

    pDC->MoveTo(lt, yi) ;

    pDC->LineTo(rt, yi) ;

    pDC->TextOut(lt-m_LH/2,yi,MakeLabel(false, y));
    }
    }

    //====== Вывод меток осей

    pDC->TextOut(lt-m_LH/2, tp - m_LH, m_sY);
    pDC->SetTextAlign(TA_LEFT | TA_BASELINE);
    pDC->TextOut(rt-m_LH, bm + m_LH, m_sX);

    //====== Вывод заголовка

    if (ra_sTitle.GetLength() > 40)

    m_sTitle.Left(40);

    pDC->SelectObject(Sm_TitleFont);
    pDC->SetTextAlign(TA_CENTER | TA_BASELINE);
    pDC->TextOut((It+rt)/2, tp - m_LH, m_sTitle);

    //====== Вывод линии графика

    DrawLine(pDC);

    //====== Восстанавливаем инструменты GDI

    pDC->RestoreDC(nDC);
    }

    Вывод линии графика начинается с создания и выбора пера. Эти действия можно вынести и поместить в какой-нибудь диалог по изменению атрибутов пера, но мы не будем этого делать, так как данное действие целесообразно только в случае, когда график состоит из нескольких линий. Обе координаты каждой точки сначала нормируются переходом к относительным значениям в диапазоне (-0.5*0.5), затем приводятся к оконным. После чего точки графика последовательно соединяются с помощью GDI-функции LineTo:

    void CGraph::DrawLine(CDC *pDC) {

    //====== Уничтожаем старое перо

    if (m_Pen.m_hObject)

    m_Pen.DeleteObject() ; //====== Создаем новое

    m_Pen.CreatePen(PS_SOLID, m_Width, m_Clr);
    pDC->SelectObject(im_Pen);

    double x0 = m_DataX.dStart,
    y0 = m_DataY.dStart,
    dx = m_DataX.dEnd - x0,
    dy = m_DataY.dEnd - y0;

    //====== Проход по всем точкам

    for (UINT i=0; i < m_Points.size(); i++) {

    //====== Нормируем координаты

    double x = (ra_Points[i].x - xO) / dx - .5,
    у = (m_Points[i].у - y0) / dy - .5;

    //====== Переход к оконным координатам

    CPoint pt (MapToLogX(x) ,MapToLogY(y)) ;

    //====== Если точка первая, то ставим перо

    if (i==0)

    pDC->MoveTo(pt);
    else

    pDC->LineTo(pt);
    }
    }

    Преобразование координат

    Преобразование координат

    В контейнере точек графика, на который ссылается переменная m_Points, хранятся мировые координаты, то есть числа, заданные в тех единицах измерения, которыми пользуется исследователь, решающий дифференциальное уравнение. Это удобно с той точки зрения, что пользователь видит и редактирует величины, к которым он привык. Для изображения графика на экране следует преобразовать мировые координаты в логические, с которыми работает подсистема GDI. На этот раз мы не будем пользоваться услугами Windows для преобразования логических координат в физические. А используем тот факт, что в режиме преобразования MMJTEXT, принятом по умолчанию, логические координаты соответствуют оконным, физическим. Мы сами будем нормировать координаты точек графика, загоняя их в диапазон (-0.5... 0.5), отслеживать изменения в размерах окна и пропорционально изменять размеры графика. По умолчанию у нас выбраны пропорции 0.6 х 0.6, что означает, размеры графика будут составлять 0.6 от размеров клиентской области окна. Преобразование координат производят два метода MapToLogX и MapToLogY. Каждый из них получает нормированную координату, а возвращает оконную. Изображение центрируется в окне с помощью переменной cpoint m_Center, значение которой должно корректироваться при каждой перерисовке. Размеры изображения зависят от текущих размеров окна (переменная csize m_size;), которые также вычисляются при каждой перерисовке:

    int CGraph::MapToLogX (double d)
    {
    return m_Center.x + int (SCALE_X * m_Size.cx * d); }

    int CGraph::MapToLogY (double d)
    {
    return m_Center.y - int (SCALE_Y * m_Size.cy * d); }

    //======= Деструктор

    CGraph::-CGraph(){}

    Деструктор не производит никаких действий, так как в классе CGraph мы не используем динамических типов данных. Контейнер точек m_Points является всего лишь ссылкой на одноименный контейнер, который хранится в классе CChildView. Там же и происходит освобождение его памяти.

    Пример с матрицей МКР

    Пример с матрицей МКР

    Для начала рассмотрим пример использования valarray л его сечения в задаче, более близкой к жизни, чем все другие учебные примеры, приводившиеся ранее. Когда-то вы слышали о том, что электронные вычислительные машины (ЭВМ) изобрели для того, чтобы решать дифференциальные уравнения. Не удивлюсь, узнав, что существуют успешно зарабатывающие на жизнь программированием молодые люди, которые об этом не знают. Однако это правда. Рассмотрим npot стое уравнение, которое способно довольно сносно описать многие процессы и явления нашей жизни j

    d/dx(p*(dU/dx))+kU=-f

    Это уравнение Пуассона и оно, например, (при k = 0 и f = 0) способно описать стационарное тепловое поле в самом простом одномерном случае, когда температура U = U(x) почему-то зависит только от одной координаты х. Например, в длинном неоднородном стержне, теплоизолированном с боков. Символ р в этом случае имеет смысл коэффициента теплопроводности материала стержня, который в принципе может зависеть от координаты р = р(х), а символ f = f(x) имеет смысл точечного источника тепла. Если f тождественно равна нулю, то мы имеем частный случай — уравнение Лапласа. Источником теплового поля в этом случае является известная температура или скорость ее изменения на границах расчетной области. Отсюда происходит термин краевая задача, то есть задача, в которой заданы граничные условия (условия на краях расчетной области). В задачах такого рода требуется найти все значения температуры внутри области. Областью расчета в одномерном случае является отрезок прямой линии. Например, слева жарко, а справа холодно. Спрашивается, а как распределена температура внутри отрезка?
    Считается, что в макромире температура распределена непрерывно, то есть в каждой точке, число которых не поддается счету, она имеет свое собственное значение. При попытке решить задачу на компьютере (сугубо дискретной структуре) надо отказаться от идеи бесконечности и ограничиться каким-то разумным числом точек, например N = 300. По опыту знаю, что график из трехсот точек вполне прилично выглядит на экране. Приняв это решение, разбивают всю область 300 точками на 299 отрезков и заменяют (аппроксимируют) производные дифференциального уравнения конечными разностями. Такова основная идея метода конечных разностей (МКР). При этом одно дифференциальное уравнение заменяется 298 алгебраическими уравнениями по числу внутренних точек, так как две граничные точки не требуют аппроксимации. Вот мы и пришли к необходимости решать систему алгебраических уравнений из 298 уравнений с 298 неизвестными температурами во внутренних точках расчетной области.

    Примечание
    Примечание

    Точно такое же уравнение описывает и многие другие физические явления. Изменяется лишь смысл параметров р и k. Например, магнитное поле в центральном поперечном сечении электрической машины с некоторыми незначительными поправками, вызванными переходом к цилиндрической системе координат, тоже с успехом может быть описано подобным уравнением.
    Для того чтобы поместить матрицу системы алгебраических уравнений в последовательность типа valarray и начать орудовать его сечениями (slice), надо сначала немного потрудиться и хотя бы понять структуру матрицы. Затем следует выработать алгоритм вычисления ее коэффициентов, и только после этого использовать динамические структуры данных и алгоритмы STL для решения задачи.

    Тем, кто почувствовал себя неуютно в незнакомой обстановке, скажем, что это нетрудно и даже интересно. Поэтому не торопитесь отбросить книгу, а продолжайте чтение и, может быть, вы еще завоюете мир, разработав великолепный инструмент для решения краевых задач в трехмерной постановке. Для начала рассмотрите схему расчетной области, которая приведена на Рисунок 11.1.



    Распределение поля для набора данных по умолчанию

    Рисунок 11.3. Распределение поля для набора данных по умолчанию

    Распределение поля для набора данных по умолчанию

    Разработка SDIприложения

    Разработка SDI-приложения
    Создайте новый проект типа MFC Application и назовите его Heat, несмотря на то что наша задача немного переросла задачу расчета стационарного теплового поля.

  • При выборе типа приложения установите переключатель Select application type в положение Single Document и снимите для разнообразия флажок Document/View architecture support.
  • На странице User Interface Features установите флажок Maximized, с тем чтобы окно приложения при начальном запуске открывалось полностью.
  • На странице Advanced Features можно снять флажок ActiveX Controls.
  • Нажмите кнопку Finish.
  • Если вам хочется увидеть, как ведет себя заготовка подобного рода, то запустите приложение (Ctrl+F5) и убедитесь, что его возможности соответствуют стандарту SDI. Откройте файл StdAfx.h и вставьте сакраментальные коды подключения нужных библиотек:

    #include using namespace std;

    Решаем краевую задачу

    Решаем краевую задачу
  • Пример с матрицей МКР
  • Метод прогонки
  • Разработка SDI-приложения
  • Класс окна для отображения графика
  • Класс графика
  • Конструктор CGraph
  • Диалог для исследования решений
  • В этом разделе мы разработаем MFC приложение с SDI-интерфейсом, которое использует контейнеры STL для хранения последовательностей величин, участвующих в формулировке простейшей одномерной краевой задачи матфизики. Сама задача формулируется в виде дифференциального уравнения, связывающего искомую функцию, пространственную координату и параметры, зависящие от свойств среды. Решение системы разностных уравнений, полученных путем аппроксимации дифференциального уравнения на сетке узлов, будет производиться методом прогонки. Контейнеры будут хранить дискретные значения коэффициентов уравнения и разностного аналога непрерывной функции.

    Схема расчетных узлов по методу МКР

    Рисунок 11.1. Схема расчетных узлов по методу МКР

    Схема расчетных узлов по методу МКР Напомним, что нашей задачей является найти значения температуры или любой другой функции U во всем множестве точек М = {1, 2, ..., N-2, N-1}, считая, что в двух точках {О, N} она известна. Переход к конечным разностям производится с помощью трехточечного шаблона, который изображен на Рисунок 11.2.



    Трехточечный шаблон аппроксимации второй производной

    Рисунок 11.2. Трехточечный шаблон аппроксимации второй производной

    Трехточечный шаблон аппроксимации второй производной Мы имеем три точки и два отрезка, которых вполне достаточно, чтобы справиться со второй производной при попытке ее приближенного вычисления. Индексы 1, г и с означают left, right и center. Обозначение pi принято для коэффициента, учитывающего свойства среды левого отрезка, например теплопроводности, а рг — правого. Шаги разбиения области вдоль оси х считаются одинаковыми и равными h. Теперь вы должны представить себе, что центр этого шаблона по очереди приставляется ко всем точкам из множества М. В результате этой процедуры мы по одному будем получать все |М| = N - 1 алгебраических уравнений, приближенно заменяющих одно дифференциальное уравнение Пуассона, которое, как говорят физики, удовлетворяется во всех точках этой части реального пространства.

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

    dU/dx=(Ur-Uc)/h
    для правого отрезка и

    dU/dx=(Uc-Ul)/h

    для левого. Теперь надо записать приближенную оценку для второй производной с учетом коэффициента теплопроводности. Он может иметь разные значения (р! и рг) в левой и в правой частях шаблона:

    d/dx(pdU/dx)={(pr[Ur-Uc])/h-(pl[Uc-Ul])/h}/h (2)

    Произведя подстановку в исходное дифференциальное уравнение (1) и упростив выражение, получим алгебраическое уравнение:

    aUl+bUc+cUr=0 (3)

    связывающее температуры трех соседних узлов сетки с физическими свойствами прилежащих участков пространства, так как значения коэффициентов уравнения зависят от р, k и h:

    a=pl/h^2; c=pr/h^2; b=-a-c+k; (4)

    Коэффициент а описывает свойства левой части шаблона, а с — правой, а Ь — обеих вместе. Чуть позже мы увидим, что коэффициент b попадет в диагональ матрицы. Теперь надо примерить шаблон ко всем узлам сетки. Узел номер 1 даст уравнение:

    a1U0+b1U1+c1U2=0,
    узел номер 2:

    a2U1+b2U2+c2U3=0,

    и т. д. Здесь важно следить за индексами. Для простоты пока считаем, что коэффициенты а,, b,, с, не изменяются при переходе от узла к узлу. В узлах сетки вблизи границ (то есть в узле 1 и узле N-1) уравнения упрощаются, так как £/„ и UN считаются известными и поэтому перемещаются в правую часть уравнения. Так, первое уравнение системы станет:

    b1U1+c1U2=0,
    а последнее:

    an-1Un-2+bn-1Un=+1=0,

    Все остальные уравнения будут иметь вид (3). Теперь пора изобразить всю систему уравнений, используя матричные обозначения и не отображая многочисленные нули. Для простоты будем считать, что N = 5:

    b1 c1 U1 -a1U0
    a2 b2 c2 U2 0
    a3 b3 c3 U3 = 0
    a4 b4 U4 -c4U5
    Вы видите, что матрица системы уравнений имеет характерную регулярную трех-диагональную структуру, а структура вектора правых частей тоже легко прочитывается. Граничные; условия краевой задачи заняли подобающие им крайние места, а внутренние позиции — нули.

    Решать такую систему следует специальным методом, который называется методом прогонки и является модификацией метода Гаусса. Он работает во много раз быстрее самого метода Гаусса. Мы реализуем его немного позже, а сейчас попробуем применить алгоритм generate из библиотеки STL для генерации матрицы, имеющей рассмотренную структуру, и вектора решений U. В простых случаях он известен и легко угадывается. Затем с помощью сечений произведем умножение Матрицы на вектор и убедимся в том, что вектор правых частей системы уравнений будет иметь правильную структуру и значения. Эту часть работы рассматривайте как дополнительное упражнение по использованию структур данных типа valarray и slice. В процессе решения краевой задачи мы будем пользоваться контейнерами другого типа (vector), так как метод прогонки не требует применения каких-то особых приемов работы с сечениями.

    Если для простоты принять р = 1, h = 1, U0 = 100, a UN =0, то коэффициенты матрицы будут равны ai = сi = 1, bi. = -2 , k = 0, а решение U(x) известно заранее. Это — линейно спадающая от 100 до 0 функция, а в более общем случае — функция произвольных граничных условий:

    U(x)=U0+[Un-U0]x/L

    где L — длина всей расчетной области. Правильность решения проверяется прямой подстановкой в уравнение (1).

    Управление параметрами краевой задачи из диалога

    Рисунок 11.5. Управление параметрами краевой задачи из диалога

    Управление параметрами краевой задачи из диалога

    Управление параметрами краевой задачи из диалога

    Управление параметрами краевой задачи из диалога

    Рисунок 11.6 Распределение поля в неоднородной среде при наличии осточнтков


    Вспомогательная функция

    Вспомогательная функция

    Напомним, что идеи, заложенные в алгоритме выработки цифровой метки на оси графика, принадлежат Александру Калимову, а сам алгоритм разрабатывался при его активном участии. Функция Make Label понадобилась нам в связи с тем, что переход к экспоненциальной форме числа требует некоторых усилий. Мы надеемся, что он будет происходить достаточно редко, так как алгоритм генерации цифровой метки использует методику «жизни без порядка в диапазоне семи порядков», описанную выше. Однако если все-таки диапазон изменения функции или даже координаты X выйдет за обозначенные пределы, то экспоненциальная форма неизбежна. При всем этом мы должны попытаться не делать метку слишком длинной. Экспоненциальная форма создается нами так, чтобы она была более компактной. Если доверить эту работу функциям системы, то они припишут ' знак порядка и лидирующие нули, от которых необходимо избавиться. Но если ее укоротить слишком сильно, то невозможно будет различить две соседние метки. Поэтому при определении количества необходимых разрядов мантиссы анализируется шаг сетки вдоль текущей оси графика. Вся эта черновая работа и производится в теле функции MakeLabel:

    CString CGraph::MakeLabel(bool bX, doubles v) {

    CString s = "0.0";

    if (v == 0.)
    return s;

    //====== Сначала делаем грубую прикидку

    //====== Пробуем поместиться в 20 позиций

    s.Format("%20.10f",v);

    //====== Выясняем порядок шага сетки,

    //====== переворачивая его знак (трюк)

    int nDigits = int(ceil(-loglO(bХ ?m_DataX.Step

    : m_DataY.Step)) ) ;

    //====== Если все изменения происходят до запятой,

    //====== то цифры после запятой нас не интересуют

    if (nDigits <= 0)

    nDigits = -1;
    else

    if(bХ)

    nDigits++; // Эстетическая добавка

    //====== Слева усекаем все

    s .TrimLeft () ;

    //====== Справа оставляем минимум разрядов

    s = s.Left(s.Find(".") + nDigits + 1);

    int iPower = bX ? m_DataX.Power : m_DataY.Power;

    ( //====== Нужен ли порядок?

    if (iPower != 0)
    {

    //=== Нужен, если не поместились в (10"-3, 10А+4)

    CString add;

    add.Format("-e%+d",iPower);

    s += add;
    }

    return s;
    }

    В настоящий момент можно запустить проект на выполнение. Вы должны увидеть распределение поля, изображенное на Рисунок 11.3.

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



    Cамоучитель по Visual Studio.Net

    Алгоритм обработки аппаратного прерывания

    Рисунок 12.3. Алгоритм обработки аппаратного прерывания

    Алгоритм обработки аппаратного прерывания
    Аппаратные прерывания могут возникать в произвольные моменты времени, и они прозрачны для прикладной программы, то есть она не знает, что была прервана в ответ на какое-то событие. Почему необходимо запоминать контекст прерванной программы и не нужно этого делать при вызове внешней функции? В последнем случае контекст не нарушается, так как программист проектирует вызов функции и сам управляет последовательностью действий, например передачей аргументов. В случае прерывания контекст выполняемой программы нарушается и уже сама система должна сделать все, чтобы восстановить текущее состояние прерванной программы, так как прерывание не было запланировано — не входило в намерения программиста. Теперь рассмотрим, как в случае прерывания прикладная программа узнает о том, что была нажата клавиша и какая (Рисунок 12.4).



    Алгоритм опроса готовности

    Рисунок 12.2. Алгоритм опроса готовности

    Алгоритм опроса готовности



    Архитектура памяти, используемая операционной системой,

    Архитектура памяти Win32

    Архитектура памяти, используемая операционной системой, — ключ к пониманию того, что в ней происходит. Не имея представления о ней, невозможно ответить на такие вопросы:

  • Как повысить эффективность приложения?
  • Как создать данные, разделяемые двумя приложениями?
  • Где хранятся системные переменные окружения?
  • Как известно, объем адресуемой памяти определяется размером регистра команд, который обычно зависит от длины машинного слова. Во времена, когда эта длина была равна 16 битам, можно было без особых ухищрений обратиться к любому байту из диапазона (0, 216-1), или 65536 = 64 Кбайт. Обращение к адресам памяти вне этого диапазона стоило определенных усилий.

    Затем, как вы помните, длина регистра команд стала равной 20 битам и появилась возможность адресовать память в диапазоне (0, 220-1) или 1 048 576 = 1 Мбайт. Правда из-за того, что длина машинного слова оставалась равной 16 битам, приходилось иметь дело с сегментами памяти по 64 Кбайт, базой, смещением, сдвигами и т. д.

    Теперь, когда наконец длина машинного слова и регистра команд стали равными 32 битам, мы можем свободно адресовать любой байт из диапазона (0, 232-1), или 4 294 967 296 = 4 Гбайт. Так как реально мы не имеем такого объема памяти, то нам предлагают научиться жить в виртуальном мире, а точнее, адресном пространстве Windows. В этом мире, как вы знаете, каждый процесс получает свое адресное пространство объемом 4 Гбайт. Корпорация Microsoft обеспечивает эту, реально не существующую, память с помощью механизма подкачки страниц памяти (page swapping), который позволяет использовать часть жесткого диска для имитации оперативной памяти. Конечно, процессор способен работать лишь с настоящей памятью типа RAM, которой ровно столько, сколько вы купили и установили, но вы можете разрабатывать приложения, не задумываясь об этом ограничении, и считать, что каждый процесс обладает пространством в 4 Гбайт. Как только в программе происходит обращение к адресу памяти, который выше реально доступного, операционная система загружает (подкачивает) недостающие данные с жесткого диска в RAM и работает с ними обычным способом.

    В MS-DOS и 16-битной Windows все процессы располагаются в едином адресном пространстве, и поэтому любой процесс может считывать и записывать любой участок памяти, в том числе и принадлежащий другому процессу или операционной системе. В таком мире состояние процесса и его благополучие зависят от поведения других процессов. В Win32 память, отведенная другим процессам, скрыта от текущего потока и недоступна ему. В Windows NT/2000 память самой ОС скрыта от любого выполняемого потока. В Windows 95 последнее свойство не реализовано, поэтому в ней текущий поток может испортить память, принадлежащую ОС.

    Итак, адресное пространство процесса — это его частная собственность, которая неприкосновенна/Поэтому первичные потоки всех процессов, одновременно существующих в физической памяти, загружаются с одного и того же адреса. В Windows NT/2000 — это 0x00400000 (4 Мбайт). Такое возможно только в виртуальном мире, в котором реальные адреса физической памяти не совпадают с виртуальными адресами в пространстве процесса. Как система отображает виртуальные адреса в реальные? Оказывается, что Windows 95 делает это не так, как Windows NT/2000. Мы будем рассматривать только последний случай, так как первый хоть и отличается от него, но эти отличия могут заинтересовать лишь ограниченный контингент разработчиков, ориентированных на разработку приложений только для Windows 95.

    Архитектура Windows

    Рисунок 12.9. Архитектура Windows

    Архитектура Windows Каждый компонент расположен в отдельном DLL-файле. Все они выполнялись в режиме user mode. Однако теперь (в NT 4.0) большинство подсистем выполняется в режиме kernel mode. При этом утверждается, что при переносе блоков из области user mode в область kernel mode надежность системы не снижается благодаря особым усилиям компании Microsoft, которая проявляет особую осторожность при создании такой части ОС, как GDD (Graphics Device Olivers).

    Примечание
    Примечание

    Вы знаете, что многие OEM-драйверы (Original Equipment Manufacturers) пишутся не в стенах компании, а другими разработчиками. Выполнение кодов этих, возможно, содержащих ошибки драйверов в режиме kernel mode, когда нет преград, может обрушить всю систему.
    Разработчики системы утверждают, что Windows NT является удивительно модульной и инкапсулированной системой, то есть слабозависящей от неожиданных изменений ситуации. Например, она не зависит от размера страницы page-файла. При загрузке системы, точнее, выполнении модуля NTDetect.com, который вы можете видеть в корневом каталоге системного диска, она определяет оптимальный размер страницы. Размер зависит от архитектуры процессора, то есть конкретной платформы. Система, например, может переключиться с размера 4К на 16К. При этом она продолжает надежно работать, несмотря на достаточно радикальную перемену в своей архитектуре.

    Функции некоторых подсистем: Virtual Memory Manager (Менеджер виртуальной памяти), Process Manager (Менеджер процессов) мы уже пояснили. Process Manager, кроме рассмотренных функций обеспечивает вместе с Virtual Memory Manager и Security Model (Модель защиты) защиту процессов друг от друга. Подсистема Object Manager (Менеджер объектов) создает, управляет и уничтожает объекты Windows NT Executive. Это абстрактные типы данных, используемые для представления таких ресурсов системы, как файлы, директории, разделяемые сегменты памяти, процессы, потоки, глобальное пространство имен и др. Благодаря модульной структуре подсистемы в нее могут быть легко добавлены и другие новые объекты.

    I/O Manager (Менеджер ввода-вывода) состоит из серии компонентов, таких как файловая система, сетевой маршрутизатор и сервер, система драйверов устройств, менеджер кэша. Стандартный интерфейс позволяет одинаковым образом общаться с любым драйвером. Здесь в полной мере проявляются преимущества многослойной (multi-layered) архитектуры.

    Много слов сказано в литературе и на конференциях про Security Reference Monitor (Монитор обращений к системе безопасности), но эта тема далеко не всем интересна. Почему-то она абсолютно не захватывает и мое воображение, хотя я понимаю, что тема может оказаться жизненно важной для тех, кому есть, что скрывать.
    Hardware Abstraction Layer (Аппаратный уровень абстракции) — HAL является изолирующим слоем между программным обеспечением, поставляемым разными производителями, и более высокими абстрактными слоями ОС. Благодаря HAL
    различные типы устройств выглядят одинаково с точки зрения системы. При этом убирается необходимость подстройки системы при введении новых устройств. При проектировании HAL была поставлена цель — создать процедуры, которые позволят общаться только с драйвером устройства, чтобы можно было управлять самим устройством в рамках любой платформы.

    Блокировки (Locks)

    Блокировки (Locks)

    Блокировки — это семафоры, которые приспособлены для двух операций транзакции (commit и abort). Они используются для обеспечения последовательного доступа конкурирующих потоков или процессов к критическим секциям. Обычно в базах данных блокируется некоторое множество данных (range of items), так как блокировка одного элемента более накладна. Представьте такой запрос:
    Select * from Customer where country = Russia and city = "Moscow";
    Чтобы защитить данные от рассмотренных выше неприятностей, надо заблокировать все строки таблицы, которые удовлетворяют указанному критерию поиска. Такой способ защиты, оказывается, обладает побочным эффектом. Он может породить запись-призрак (phantom). Допустим, что в это же время другой поток процесса добавляет в ту же таблицу Customer (Клиент) новую запись и ее поля удовлетворяют тому же критерию (клиент из Мосвы). Она, конечно же, будет добавлена в таблицу, но для первого потока она является фантомом (не существует).

    Диаграмма асинхронного взаимодействия

    Рисунок 12.4. Диаграмма асинхронного взаимодействия

    Диаграмма асинхронного взаимодействия Обработчик аппаратного прерывания от клавиатуры преобразует код нажатия клавиши в код, понятный прикладной программе (ASCII), и помещает его в буфер. Прикладная программа не реагирует на нажатие клавиши и не замечает вызова обработчика, но когда у нее возникает желание выяснить, нажимал ли пользователь какие-нибудь клавиши, она вызывает системную функцию. Последняя анализирует буфер и, если он не пуст, возвращает код нажатой клавиши или признак того, что буфер пуст.

    Диаграмма взаимодействия ОС с прикладной программой

    Рисунок 12.1. Диаграмма взаимодействия ОС с прикладной программой

    Диаграмма взаимодействия ОС с прикладной программой Системный модуль, который подготавливает запуск прикладной программы, с тем чтобы вернуть управление обратно после ее завершения, называется program prefix segment. Он, так же как и модуль завершения программы пользователя, занимает определенное время и другие ресурсы системы. Если в процессе выполнения пользовательской прбграмме нужно выполнить какое-либо стандартное действие, например вывести строку символов на принтер, она может обратиться к стандартной подпрограмме, входящей в состав операционной системы. Такие стандартные действия, реализованные в ОС в виде отдельных процедур, принято называть системными сервисами. Обратите внимание на тот факт, что прикладная программа сама определяет момент предоставления ей системного сервиса. Программа при завершении, как вы знаете, может вернуть системе некий код (успех или неудача). Как программа может узнать о наступлении какого-либо события, внешнего по отношению к ней, например такого, как нажатие пользователем клавиши клавиатуры? Существуют два способа: по опросу готовности и с помощью механизма аппаратных прерываний. Клавиатура является устройством, о котором можно программно, анализируя содержимое программно-доступного регистра состояния клавиатуры, узнать, готово ли оно к обмену, то есть нажата ли клавиша. Алгоритм процедуры обмена, соответствующий первому способу, изображен на Рисунок 12.2. Он обладает тем недостатком, что при ожидании процессор используется неэффективно, то есть простаивает. Второй способ иллюстрируется Рисунок 12.3.

    В результате какого-либо события: нажатия клавиши клавиатуры, движения мыши, срабатывания таймера, вырабатывается сигнал. По этому сигналу процессор прерывает выполнение текущей программы, запоминает состояние (контекст) прерванной программы и передает управление программе-обработчику возникшей ситуации.



    Диапазон изменения приоритета потока

    Рисунок 12.7. Диапазон изменения приоритета потока

    Диапазон изменения приоритета потока При наличии нескольких процессоров Windows NT применяет симметричную модель распределения потоков по процессорам (symmetric multiprocessing SMP), Это означает, что любой поток может быть направлен на любой процессор, но программист может ввести некоторые коррективы в эту модель равноправного распределения. Функции SetProcessAffinityMask и SetThreadAffinityMask позволяют указать предпочтения в смысле выбора процессора для всех потоков процесса или для одного определенного потока. Потоковое предпочтение (thread affinity) вынуждает систему выбирать процессоры только из множества, указанного в маске. Существует также возможность для каждого потока указать один предпочтительный процессор. Это делается с помощью функции SetThreadidealProcessor. Это указание служит подсказкой для планировщика заданий, но не гарантирует строгого соблюдения.

    Иерархия классов синхронизации

    Рисунок 12.10. Иерархия классов синхронизации

    Иерархия классов синхронизации Все перечисленные классы, кроме критической секции, принадлежат ядру Windows. Вы знаете, что Windows-приложение использует множество и других объектов:

  • окна, меню, курсоры, значки, клавиатурные ускорители и т.д. (объекты GUI или Graphics User Intrface);
  • перья, кисти, растровые рисунки, шрифты (объекты GDI Graphics Device Interface).
  • При работе с объектами этих подсистем надо соблюдать определенные правила. Но при работе с объектами ядра правила особые. Вам следует познакомиться с общими положениями об использовании объектов ядра системы. Они похожи на стандарты СОМ.

  • Однажды созданный объект ядра можно открыть в любом приложении, если оно имеет соответствующие права доступа к нему.
  • Каждый объект ядра имеет счетчик числа своих пользователей. Как только он станет равным нулю, система уничтожит объект ядра.
  • Обращаться к объекту ядра надо через описатель (handle), который система дает при создании объекта.
  • Каждый объект может находиться в одном из двух состояний: свободном (signaled) и занятом (nonsignaled).
  • Синхронизация потоков развивается по такому сценарию. При засыпании одного из них операционная система перестает выделять ему кванты процессорного времени, приостанавливая его выполнение. Прежде чем заснуть, поток сообщает системе то особое событие, которое должно разбудить его. Как только указанное событие произойдет, система возобновит выдачу ему квантов процессорного времени и ноток вновь получит право на жизнь. Потоки усыпляют себя до освобождения какого-либо синхронизирующего объекта с помощью двух функций:

    DWORD WaitForSingleObject (HANDLE hObject, DWORD dwTimeOut);
    DWORD WaitForMultipleObjects(DWORD nCount,

    CONST HANDLE* lpHandles, BOOL bWaitAll,

    DWORD dwTimeOut);

    Первая функция приостанавливает поток до тех пор, пока или заданный параметром hObject синхронизирующий объект не освободится, или пока не истечет интервал времени, задаваемый параметром dwTimeOut. Если указанный объект в течение заданного интервала не перейдет в свободное состояние, то система вновь активизирует поток и он продолжит свое выполнение. В качестве параметра dwTimeOut могут выступать два особых значения:

    Таблица 12.3. Значения, выступающие в качестве параметра dwTimeOut

    dwTime Out Описание

    0 Функция только проверяет состояние объекта (занят или свободен) и сразу же возвращается
    INFINITE


    Время ожидания бесконечно. Если объект так и не освободится, поток останется в неактивном состоянии и никогда не получит процессорного времени

    В соответствии с причинами, по которым поток продолжает выполнение, функция WaitForSingleObject может возвращать одно из следующих значений:

    Таблица 12.4. Возвращение значений функцией WaitForSingleObject

    Возвращаемое значение

    Описание

    WAITJ1MEOUT Объект не перешел в свободное состояние, но интервал времени истек
    WAIT_ABANDONED Ожидаемый объект является мьютексом, который не был освобожден владеющим им потоком перед окончанием этого потока. Объект мьютекс автоматически переводится системой в состояние свободен. Такая ситуация называется «отказ от мьютекса»
    WAIT_OBJECT_0 Объект перешел в свободное состояние
    WAIT_FAILED

    Произошла ошибка, причину которой можно узнать, вызвав GetLastError
    Функция WaitForMultipleObjects задерживает поток и в зависимости от значения флага bWaitAll ждет одного из следующих событий:

  • освобождение хотя бы одного синхронизирующего объекта из заданного списка;
  • освобождение всех указанных объектов;
  • истечение заданного интервала времени.


  • Критические секции

    Критические секции

    Это самые простые объекты ядра Windows, которые не снижают общей эффективности приложения. Пометив блок кодов в качестве critical section, можно синхронизировать доступ к нему от нескольких потоков. Сначала следует объявить глобальную структуру;
    CRITICAL_SECTION cs;
    затем инициализировать ее вызовом функции Initial! zeCri ticalSection (&cs);. Обычно это делается один раз, перед тем как начнется работа с разделяемым ресурсом. Далее надо поместить охраняемую часть программы внутрь блока, который начинается вызовом функции EnterCriticalSection и заканчивается вызовом LeaveCriticalSection:

    EnterCriticalSection (&cs);
    {
    //====== Здесь расположен охраняемый блок кодов

    }

    LeaveCriticalSection (Scs);
    Функция EnterCriticalSection, анализируя поле структуры cs, которое является счетчиком ссылок, выясняет, вызвана ли она в первый раз. Если да, то функция увеличивает значение счетчика и разрешает выполнение потока дальше. При этом выполняется блок, модифицирующий критические данные. Допустим, в это время истекает квант времени, отпущенный данному потоку, или он вытесняется более приоритетным потоком, использующим те же данные. Новый поток выполняется, пока не встречает функцию EnterCriticalSection, которая помнит, что объект cs уже занят. Новый поток останавливается (засыпает), а остаток процессорного времени передается другому потоку.
    Функция LeaveCriticalSection уменьшает счетчик ссылок на объект cs. Как только поток покидает критическую секцию, счетчик ссылок обнуляется и система будит ожидающий поток, снимая защиту секции кодов. Критические секции применяются для синхронизации потоков лишь в пределах одного процесса. Они управляют доступом к данным так, что в каждый конкретный момент времени только один поток может их изменять. Когда надобность в синхронизации потоков отпадает, следует вызвать DeleteCriticalSection (&cs);. Эта функция освобождает все ресурсы, включенные в критическую секцию.

    Механизмы синхронизации

    Механизмы синхронизации

    Существует несколько стратегий, которые могут применяться, чтобы разрешать описанные проблемы. Наиболее распространенным способом является синхронизация потоков. Суть синхронизации состоит в том, чтобы вынудить один поток ждать, пока другой не закончит какую-то определенную заранее операцию. Для этой цели существуют специальные синхронизирующие объекты ядра операционной системы Windows. Они исключают возможность одновременного доступа к тем данным, которые с ними связаны. Их реализация зависит от конкретной ситуации и предпочтений программиста, но все они управляют потоками процесса по принципу: «Не все сразу, по одному, ребята».
    MFC предоставляет несколько классов, реализующих механизмы синхронизации. Прежде всего отметим, что хорошо спроектированный {thread-safe) класс не должен требовать особых затрат для синхронизации работы с ним. Все делается внутри класса его методами. Обычно при создании надежного класса в него изначально внедряют какой-либо синхронизирующий объект. Например, критическую секцию, событие, семафор, мъютекс или ожидаемый таймер. Иерархию классов MFC для поддержки синхронизирующих объектов можно увидеть в MSDN:



    Мьютексы (Mutexes)

    Мьютексы (Mutexes)

    Критические секции просты в использовании и обладают высоким быстродействием, но не обладают гибкостью в управлении. Нет, например, возможности установить время блокирования или присвоить имя критической секции для того, чтобы два разных процесса могли иметь с ней дело. Оба эти недостатка можно устранить, если использовать такой объект ядра, как mutex. Термин mutex происходит от mutually exclusive (взаимно исключающий). Этот объект обеспечивает исключительный (exclusive) доступ к охраняемому блоку кодов. Например, если несколько процессов должны одновременно работать с одним и тем же связным списком, то на время выполнения каждой операции: добавления, удаления элемента или сортировки, следует заблокировать список и разрешить доступ к нему только одному из процессов.
    Для синхронизации потоков разных процессов следует объявить один общедоступный объект класса CMutex, который будет управлять доступом к списку. Мыо-текс предоставляет доступ к объекту любому из потоков, если в данный момент объект не занят, и запоминает текущее состояние объекта. Если объект занят, то мьютекс запрещает доступ. Однако можно подождать освобождения объекта с помощью функции WaitForSingleObject, в которой роль управляющего объекта выполняет тот же мьютекс. Типичная тактика использования такова. Объект
    CMutex mutex;

    необходимо объявить заранее. Обычно он является членом thread-safe-класса. В точке, где необходимо защитить код, создается объект класса CSingleLock, которому передается ссылка на мьютекс. При попытке включения блокировки вызовом метода Lock надо в качестве параметра указать время (в миллисекундах), в течение которого следует ждать освобождения объекта, охраняемого мьютексом. В течение этого времени либо получим доступ к объекту, либо не получим его. Если объект стал доступен, то мы запираем его от других потоков и производим работу, которая требует синхронизации. После этого освобождаем блокировку. Если время ожидания истекло и доступ к объекту не получен, то обработка этой ситуации (ветвь else) целиком в нашей власти. Если задать ноль в качестве параметра функции Lock, то ожидания не будет. Напротив, можно ждать неопределенно долго, если передать константу INFINITE.
    Другой процесс, если он знает, что существует мьютекс с каким-то именем, может сделать этот объект доступным для себя, открыв уже существующий мьютекс. При вызове функции OpenMutex система сканирует существующие объекты-мьютексы, проверяя, нет ли среди них объекта с указанным именем. Обнаружив таковой, она создает описатель объекта, специфичный для данного процесса. Теперь любой поток данного процесса может использовать описатель в целях синхронизации доступа к какому-то коду или объекту. Когда мьютекс становится ненужным, следует освободить его вызовом
    CloseHandle(HANDLE hObject);
    где hObject — описатель мьютекса. Когда система создает мьютекс, она присваивает ему имя (строка в стиле С). Это имя используется при совместном доступе к мыотексу нескольких процессов. Если несколько потоков создают объект с одним и тем же именем, то только первый вызов приводит к созданию мьютекса. Имя используется при совместном доступе нескольких процессов. Если оно совпадает с именем уже существующего объекта, конструктор создает новый экземпляр класса CMutex, который ссылается на существующий мьютекс с данным именем. Если имя не задано (IpszName равен NULL) мьютекс будет неименованным, и им можно пользоваться только в пределах одного процесса.
    С любым объектом ядра сопоставляется счетчик, фиксирующий, сколько раз данный объект передавался во владение потокам. Если поток вызовет, например, CSingleLock: :Lock() ИЛИ WaitForSingleObject () ДЛЯ уже принадлежащего ему объекта, он сразу же получит доступ к защищаемым этим объектом данным, так как система определит, что поток уже владеет этим объектом. При этом счетчик числа пользователей объекта увеличится на 1. Теперь, чтобы перевести объект в свободное состояние, потоку необходимо соответствующее число раз вызвать CSingleLock::Unlock() ffilHReleaseMutex() . Функции EnterCriticalSection и LeaveCriticalSection действуют по отношению к критическим секциям аналогичным образом.
    Объект-мьютекс отличается от других синхронизирующих объектов ядра тем, что занявшему его потоку передаются права на владение им. Прочие синхронизирующие объекты могут быть либо свободны, либо заняты и только, а мьютексы способны еще и запоминать, какому потоку они принадлежат. Отказ от мьютекса происходит, когда ожидавший его поток захватывает этот объект, переводя его в занятое состояние, а потом завершается. В таком случае получается, что мьютекс занят и никогда не освободится, поскольку другой поток не сможет этого сделать. Система не допускает подобных ситуаций и, заметив, что произошло, автоматически переводит мьютекс в свободное состояние.

    Многозадачные операционные системы

    Многозадачные операционные системы

    Почти все современные операционные системы (Windows 95, Windows NT, Windows 2000, Unix) поддерживают преимущественную многозадачность {preemptive multi-tasking). Этот термин, который часто переводят как вытесняющая многозадачность, означает, что процесс или, точнее, его поток, который в данный момент активен, имеет преимущество перед другими конкурирующими потоками с одинаковым приоритетом. Системы Windows 3.1 и Macintosh поддерживают кооперативную многозадачность {cooperative multi-tasking), в которой все управление отдано системе. В такой системе легче программировать, но она менее эффективна.

    Основным признаком многозадачной ОС является способность совмещать выполнение нескольких прикладных программ. Большое значение при этом имеет способ совмещения, то есть на каком уровне или как конкретно реализовано совмещение. Если однопроцессорная, но многозадачная, система выделяет каждой прикладной программе определенный квант времени {lime slice), спустя который она переключается на выполнение следующей программы, то это система с разделением времени {time-sharingsystem). Системы с разделением времени появились в начале 60-х. Они управлялись main /rame-компьютерами, обслуживающими многочисленные удаленные терминалы. В качестве терминалов сначала использовались обычные телетайпы, которые умели только вводить или выводить информацию. Благодаря огромной разнице в скорости работы таких устройств, как телетайп и процессор, системы с разделением времени успевали переключаться между многими терминалами и вводить или выводить информацию так, что каждому пользователю казалось, что он единолично управляет удаленным процессором. Затем появились персональные компьютеры, которые стали использоваться в качестве удаленных терминалов. В связи с этим для операционной системы главного процессора (например, IBM-370) отпала необходимость заниматься посимвольным вводом-выводом. Теперь акцент в разработке операционных систем был перенесен на управление выполняемыми программными модулями, принадлежащими разным пользователям и одновременно находящимися в памяти главного компьютера. Появились такие понятия, как очередь заданий •-- очередь на обслуживание каким-либо устройством: принтером, плоттером, накопителем на магнитном носителе, приоритет задания, ожидаемое время завершения задания и т. д.

    В настоящее время, когда каждый пользователь имеет достаточно мощный персональный компьютер, акценты в развитии ОС снова изменились. Теперь большое значение приобретает развитие сетевых, многозадачных ОС. В сущности, теперь пользователь имеет возможность установить на отдельном персональном компьютере многозадачную ОС и разрабатывать приложения, совмещающие вы-полнение нескольких процессов. Каждый процесс, в свою очередь, может состоять из нескольких потоков, выполняемых в адресном пространстве процесса.
    Первые операционные системы, реализованные на персональных компьютерах, сильно уступали в концептуальном плане и по своим реальным возможностям системам с разделением времени, давно реализованным в mainframe- компьютерах. В Win 16, например, тоже существует понятие многозадачности. Реализовано оно следующим образом: обработав очередное сообщение, приложение передает управление операционной системе, которая может передать управление другому приложению. Такой вид многозадачности, при котором операционная система передает управление от одного приложения другому не в любой момент времени, а только когда текущее приложение отдает управление системе, получил, как было упомянуто, название кооперативной многозадачности (cooperative multi-tasking).
    Если при таком подходе обработка сообщения затягивается, то пользователь увидит реакцию системы только после завершения обработки текущим приложением • текущего сообщения. Обычно при выполнении длительных операций программист изменяет форму курсора (песочные часы), вызвав API-функцию BeginWaitCursor. Иногда, если это предусмотрел разработчик программы, в таких случаях застрявшее приложение даже вызывает функцию PeekMessage, сообщая системе, что она может обработать очередное сообщение, а текущее приложение способно и подождать. Но главная неприятность при таком подходе заключается в том, что в случае бесконечного цикла, вызванного ошибкой в программе, ОС не имеет шансов получить управление и также зависнет. Пользователю придется перезагружать систему.
    В Windows начиная с Windows 95 реализован принципиально другой вид многозадачности, в котором операционная система действительно контролирует и управляет процессами, потоками и их переключением. Способность операционной системы прервать выполняемый поток практически в любой момент времени и передать управление другому ожидающему потоку определяется термином preemptive multitasking — преимущественная, или вытесняющая, многозадачность. Реализация ее выглядит так: все существующие в данный момент потоки, часть из которых может принадлежать одному и тому же процессу, претендуют на процессорное время и, с точки зрения пользователя должны выполняться одновременно. Для создания этой иллюзии система через определенные промежутки времени забирает управление, анализирует свою очередь сообщений, распределяет сообщения по другим очередям в пространстве процессов и, если считает нужным, переключает потоки (Рисунок 12.5).
    Реализация вытесняющей многозадачности в Windows 2000 дает не только возможность плавного переключения задач, но и устойчивость среды к зависаниям, так как ни одно приложение не может получить неограниченные права на процессорное время и другие ресурсы. Так система создает эффект одновременного выполнения нескольких приложений. Если компьютер имеет несколько процессоров, то системы Windows NT/2000 могут действительно совмещать выполнение нескольких приложений. Если процессор один, то совмещение остается иллюзией. Когда заканчивается квант времени, отведенный текущей программе, система ее прерывает, сохраняет контекст и отдает управление другой программе, которая ждет своей очереди. Величина кванта времени (time slice) зависит от ОС и типа процессора, в Windows NT она в среднем равна 20 мс. Следует отметить, что добиться действительно одновременного выполнения потоков можно только на машине с несколькими процессорами и только под управлением Windows NT/2000, ядра которых поддерживают распределение потоков между процессорами и процессорного времени между потоками на каждом процессоре. Windows 95 работает только с одним процессором. Даже если у компьютера несколько процессоров, под управлением Windows 95 задействован лишь один из них, а остальные простаивают.



    Некоторые сведения об архитектуре Windows

    Некоторые сведения об архитектуре Windows
  • Windows 2000 — многозадачная операционная система
  • Уровни и платформы
  • Однозадачные и многозадачные ОС
  • Процессы и потоки
  • Архитектура памяти Win32
  • Подсистемы ОС и их взаимодействие
  • Разделяемые ресурсы
  • Механизмы синхронизации


  • Однозадачные операционные системы

    Однозадачные операционные системы

    Известно, что в каждый момент времени один процессор может выполнять лишь одну машинную команду. Процессор выполняет команды, последовательно выбирая их одну за другой из памяти в порядке возрастания адресов. Программист может нарушить этот порядок, вставив в программу команду условного или безусловного перехода, команду вызова функции или цикла (bne, call, jmp, loop). Операционная система, даже однозадачная, также может вмешаться в эту последовательность и отвлечься — временно оторваться от последовательности команд для выполнения каких-то других более важных, системных команд. Необходимость такой операции, называемой прерыванием, выясняется в процессе выборки очередной команды. Если система обнаруживает, что есть прерывание, то она запоминает в стеке контекст выполняемой программы: адрес текущей команды, ' содержимое регистров АУ, и переходит в режим обработки прерывания, то есть переключается на выполнение другой программы, вызвавшей прерывание. В системе команд существуют также особые, привилегированные инструкции, которые невозможно прервать. Они называются atomic instructions, (вы помните, что атом переводится как неделимый) и используются системой при выполнении критических для целостности системы процедур.

    В оперативной памяти (RAM), даже в случае однозадачной ОС (операционная система), могут одновременно находиться несколько программ:

  • резидентная часть операционной системы; %
  • резидентные программы, которые запускает система или пользователь;
  • прикладная программа, выполняемая в данный момент.
  • В простейшем случае однозадачной ОС, такой как MS DOS, взаимодействие между тремя объектами, увязываемыми ОС, может протекать так, как показано на Рисунок 12.1.



    Переключение потоков в системе с вытесняющей многозадачностью

    Рисунок 12.5. Переключение потоков в системе с вытесняющей многозадачностью

    Переключение потоков в системе с вытесняющей многозадачностью

    Переключение потоков

    Переключение потоков
    Планировщик ОС поддерживает для каждого из базовых уровней приоритета функционирование очереди выполняемых или готовых к выполнению потоков (ready threads queue). Когда процессор становится доступным, то планировщик производит переключение контекстов. Здесь можно выделить такие шаги:

  • сохранение контекста потока, завершающего выполнение; О перемещение этого потока в конец своей очереди;
  • поиск очереди с высшим приоритетом, которая содержит потоки, готовые к выполнению;
  • выбор первого потока из этой очереди, загрузка его контекста и запуск на выполнение.
  • Примечание
    Примечание

    Если в системе за каждым процессором закреплен хотя бы один поток с приоритетом 31, то остальные потоки с более низким приоритетом не смогут получить доступ к процессору и поэтому не будут выполняться. Такая ситуация называется starvation.

    Различают потоки, не готовые к выполнению. Это:

  • потоки, которые при создании имели флаг CREATE_SUSPENDED;
  • потоки, выполнение которых было прервано вызовом функции SuspendThread или SwitchToThread;
  • потоки, которые ожидают ввода или синхронизирующего события.
  • Блокированные таким образом потоки или подвешенные (suspended) потоки не получают кванта времени независимо от величины их приоритета. Типичными причинами переключения контекстов являются следующие:

  • истек квант времени,
  • в очереди с более высоким приоритетом появился поток, готовый к выполнению,
  • текущий поток вынужден ждать.
  • В последнем случае система не ждет завершения кванта времени и отнимает управление, как только поток впадает в ожидание. Возможный вариант развития событий изображен на Рисунок 12.6.

    Кроме рассмотренного базового уровня каждый поток обладает динамическим приоритетом. Под этим понятием скрываются временные колебания уровня, которые вызваны планировщиком. Он намеренно вызывает такие колебания, для того чтобы убедиться в управляемости и реактивности потока, а также для того, чтобы дать шанс потокам с низким приоритетом. Система никогда не подстегивает потоки, приоритет которых итак высок (от 16 до 31). Когда пользователь работает с каким-то процессом, то он считается активным (foreground), а остальные процессы — фоновыми (background). При ускорении потока (priority boost) система действует следующим образом: когда процесс с нормальным классом приоритета «выходит на сцену» (is brought to the foreground), он получает ускорение.



    Подсистемы ОС

    Подсистемы ОС

    Операционная система Windows NT представляет собой множество отдельных модулей (подсистем), которые разработаны с учетом двух фундаментальных принципов:

  • модульность, инкапсуляция, скрытие данных, :
  • некоторые подсистемы функционируют в привилегированном режиме процессора (kernel mode), а остальные в режиме (user mode).
  • Первый принцип подразумевает, что каждая подсистема отвечает за отдельную функцию всей системы и все другие потоки — другие части ОС или приложения пользователя, общаются с ней с помощью одного и того же хорошо продуманного интерфейса. Реализация принципа делает невозможными какие-то другие способы (back doors) доступа к критическим для функционирования системы структурам данных. Кроме того, такой подход дает возможность легко производить усовершенствование (upgrade) системы, так как подсистемы, удовлетворяющие заранее известному интерфейсу, можно заменять без какого-либо ущерба для системы.

    Для оценки важности второго принципа необходимо пояснить суть режимов выполнения команд kernel mode и user mode. В режиме ядра (kernel mode) вся память доступна и все команды выполнимы. Это привилегированный режим по сравнению с режимом user mode, когда система проверяет права доступа потока при каждом его обращении к памяти. Режим выполнения user mode значительно более надежен, но требует дополнительных затрат, которые снижают общую производительность процесса. В литературе режим ядра иногда называют режимом супервизора или режимом Ring(). Степени защиты памяти называют кольцами, а нулевое кольцо обозначает самый привилегированный аппаратный уровень. Вы можете встретить также обозначения PL=0 (Privilege Level) для kernel mode и PL = 3 для user mode. Если операционная система выполняет первый принцип и большинство ее модулей выполняется в режиме user mode, то говорят, что ОС является риге microkemel-системой. Возможны две версии перевода: «имеет чистое микроядро» и «настоящая microkernel-система». Если система удовлетворяет только первому принципу, то ее называют macrokernel OS. Большинство коммерческих ОС не выполняет второй принцип, так как они хотят быть быстрыми. Windows сразу примкнула к сторонникам microkernel OS, так как здесь соображения надежности поставлены на более высокое место.

    На Рисунок 12.9 приведена схема, иллюстрирующая архитектуру (состав подсистем) Windows NT. Подсистема Win32 Subsystem состоит из пяти модулей:

  • Window Manager—компонент, который управляет вводом и выводом на экран. Этот модуль имеет и другое имя — User. Он и располагается в библиотеке User32.dll;
  • Graphics Device Interface (GDI) — библиотека функций и структур, которые реализуют рисование в контексте устройства. Контекст устройства — это логическая структура, не зависящая от физического устройства и позволяющая пользоваться максимальными возможностями и средствами для вывода графики. Вывод в конкретное физическое устройство производится с помощью драйвера устройства. Система при этом преобразовывает и, возможно, искажает информацию с учетом ограничений, характерных для конкретного устройства. Поэтому реальная картина может отличаться от идеальной, созданной в контексте устройства. Различают шесть инструментов GDI, с помощью которых осуществляется рисование: Pen, Brush, Font, Bitmap, Palette и Region;
  • Graphics Device Drivers (GDD) — аппаратно-зависимые драйверы, которые осуществляют связь с конкретными физическими устройствами ввода и вывода;
  • Console — компонент, который поддерживает текстовый режим вывода в окно;
  • Operating System Functions — функции, которые поддерживают все другие компоненты подсистемы Win32.


  • Приоритеты потоков

    Приоритеты потоков

    Теперь рассмотрим уровни приоритета, которые могут быть присвоены потокам процесса. Внутри каждого процесса, которому присвоен какой-либо класс приоритета, могут существовать потоки, уровень приоритета которых принимает одно из семи возможных значений:
  • THREAD_PRIORITY_IDLE;
  • THREAD_PRIORITY_LOWEST;
  • THREAD_PRIORITY__BELOW___NORMAL;
  • THREAD_PRIORITY_NORMAL;
  • THREAD_PRIORITY_ABOVE_NORMAL;
  • THREAD_PRIORITY_HIGHEST;
  • THREAD_PRIORITY_TIME_CRITICAL.
  • Все потоки сначала создаются с уровнем THREAD_PRIORITY_NORMAL. Затем программист может изменить этот начальный уровень, вызвав функцию SetThreadPriority.
    Типичной стратегией является повышение уровня до ...ABOVE_NORMAL или ...HIGHEST для потоков, которые должны быстро реагировать на действия пользователя по вводу информации. Потоки, которые интенсивно используют процессор для вычислений, часто относят к фоновым. Им дают уровень приоритета ...BELOW_NORMAL или ...LOWEST, так чтобы при необходимости они могли быть вытеснены. Иногда возникает ситуация, когда поток с более высоким приоритетом должен ждать поток с низким приоритетом, пока тот не закончит какую-либо операцию. В этом случае не следует программировать ожидание завершения операции в виде цикла, так как львиная доля времени процессора уйдет на выполнение команд этого цикла. Возможно даже зацикливание — ситуация типа deadlock, так как поток с более низким приоритетом не имеет шанса получить управление и завершить операцию. Обычной практикой в таких случаях является использование:

  • одной из функций ожидания (wait functions); О вызов функции Sleep (sleepEx); О вызов функции SwitchToThread;
  • использование объекта типа critical section (критическая секция), который мы рассмотрим позже.
  • Для определения текущего уровня приоритета потока существует функция GetThreadPriority, которая возвращает один из семи рассмотренных уровней. Базовый приоритет потока, как было упомянуто, является комбинацией класса приоритета процесса и уровня приоритета потока. Он вычисляется в соответствии с таблицей, которая довольно объемна (47 строк) и поэтому здесь не приводится. Просмотрите ее в справке (Help), в разделе Platform SDK-Scheduling Priorities (Платформа, SDK-Планирование приоритетов). К примеру, первичный поток процесса с классом HIGH_PRIORITY_CLASS по умолчанию получает начальное значение уровня приоритета THREAD_PRIORITY_NORMAL. Эта комбинация образует базовый уровень приоритета, равный 13. Если впоследствии вы присвоите потоку с помощью функции SetThreadPriority уровень...ьоиЕЗТ, то эта комбинация задаст базовый уровень 11. Если же вы для потока выберете уровень ...IDLE, то базовый уровень скакнет и опустится до единицы. Считая, что класс приоритета процесса не изменяется и остается равным HIGH_PRIORITY_CLASS, сведем все семь возможных вариантов в табл. 12.1.

    Таблица 12.1. Приоритеты потоков

    Уровень приоритета потока

    Базовый уровень

    THREAD_PRIORITY_IDLE

    1

    THREAD_PRIORITY_LOWEST

    11

    THREAD_PRIORITY_8ELOW_NORMAL

    12

    THREAD_PRIORITY_NORMAL

    13

    THREAD_PRIORITY_ABOVE_NORMAL

    14

    THREAD_PRIORITY_HIGHEST

    15

    THREAD_PRIORITY_TIME_CRITICAL

    15



    Приоритеты процессов

    Приоритеты процессов

    Часть ОС, называемая системным планировщиком (system scheduler), управляет переключением заданий, определяя, какому из конкурирующих потоков следует выделить следующий квант времени процессора. Решение принимается с учетом приоритетов конкурирующих потоков. Множество приоритетов, определенных в ОС для потоков, занимает диапазон от 0 (низший приоритет) до 31 (высший приоритет). Нулевой уровень приоритета система присваивает особому потоку обнуления свободных страниц. Он работает при отсутствии других потоков, требующих внимания со стороны ОС. Ни один поток, кроме него, не может иметь нулевой уровень. Приоритет каждого потока определяется в два этапа исходя из:

  • класса приоритета процесса, в контексте которого выполняется поток, О уровня приоритета потока внутри класса приоритета потока.
  • Комбинация этих параметров определяет базовый приоритет (base priority) потока. Существует шесть классов приоритетов для процессов:

  • IDLE_PRIORITY_CLASS,
  • BELOW_NORMAL_PRIORITY_CLASS,
  • NORMAL__PRIORITY_CLASS,
  • ABOVE_NORMAL_PRIORITY_CLASS,
  • HIGH_PRIORITY_CLASS,
  • REALTIME_PRIORITY_CLASS
  • Два класса (BELOW... и ABOVE...) появились начиная с Windows NT 5.0. По умолчанию процесс получает класс приоритета NORMAL_PRIORITY__CLASS. Программист может задать класс приоритета создаваемому им процессу, указав его в качестве одного из параметров функции CreateProcess. Кроме того, существует возможность динамически, во время выполнения потока, изменять класс приоритета процесса с помощью API-функции SetPriorityClass. Выяснить класс приоритета какого-либо процесса можно с помощью API-функции GetPriorityClass.
    Процессы, осуществляющие мониторинг системы, а также хранители экрана (screen savers) должны иметь низший класс (IDLE...), чтобы не мешать другим полезным потокам. Процессы самого высокого класса (REALTIME...) способны прервать даже те системные потоки, которые обрабатывают сообщения мыши, ввод с клавиатуры и фоновую работу с диском. Этот класс должны иметь только те процессы, которые выполняют короткие обменные операции с аппаратурой.
    Если вы пишете драйвер какого-либо устройства, используя API-функции из набора DDK (Device Driver Kit), то ваш процесс может иметь класс REALTIME... С осторожностью следует использовать класс HIGH_PRIORITY_CLASS, так как если поток процесса этого класса подолгу занимает процессор, то другие потоки не имеют шанса получить свой квант времени. Если несколько потоков имеют высокий приоритет, то эффективность работы каждого из них, а также всей системы резко падает. Этот класс зарезервирован для реакций на события, критичные ко времени их обработки. Обычно с помощью функции SetPriorityClass процессу временно присваивают значение HIGH..., затем, после завершения критической секции кода, его снижают. Применяется и другая стратегия: создается процесс с высоким классом приоритета и тотчас же блокируется — погружается в сон с помощью функции Sleep. При возникновении критической ситуации поток или потоки этого процесса пробуждаются только на то время, которое необходимо для обработки события.

    Процессы и потоки

    Процессы и потоки

    Различают два способа реализации многозадачности:
  • создать один процесс, имеющий несколько потоков выполнения (threads);
  • создать несколько процессов, каждый из которых имеет один или несколько потоков выполнения.
  • Многозадачная (multi-process) система позволяет двум или более программам выполняться одновременно. Многопотоковая (multi-threaded) система позволяет одной программе выполнять сразу несколько потоков одновременно. Современные операционные системы сочетают в себе оба эти свойства. Приложение Win32 может состоять из одного или более процессов. Например, приложение по расчету параметров турбогенератора может состоять из удобной оболочки, написанной на языке C++ (главный процесс), и вычислительных модулей, написанных на языке FORTRAN и запускаемых в виде отдельных (порожденных) процессов. При этом возможен вариант, когда один процесс (модуль программы) занят выводом геометрии расчетной области, а другой одновременно производит расчет электромагнитного поля.
    Процесс — это понятие, относящееся к операционной системе. Каждый раз, как вы запускаете приложение, система создает и запускает новый процесс. Процесс можно грубо отождествить с ехе-кодом, выполняющимся в отдельном процессоре. С каждым процессом система связывает такие ресурсы, как:

  • виртуальное адресное пространство;
  • исполнимый код и данные;
  • базовый приоритет;
  • описатели объектов;
  • переменные окружения.
  • Windows NT/2000 отводит для каждого процесса виртуальное адресное пространство в 4 Гбайт, защищенное от других процессов, которые выполняются в системе в то же самое время.
    Каждый процесс обязательно создает первичный поток (primary thread) выполнения. Он делает это автоматически и, если программист не предпринимает каких-либо специальных усилий по созданию второго потока, то первичный поток и породивший его процесс обычно отождествляются в сознании пользователя, а , часто и в сознании программиста. Но последний может создать еще один или несколько потоков, которые размещаются в одном и том же адресном пространстве, принадлежащем процессу. Когда они создаются, родительский процесс начинает выполняться не последовательно, а параллельно. Так реализуется потоковая многозадачность. Говорят, что потоки выполняются в контексте процесса.
    Поток (thread) — это основной элемент системы, которому ОС выделяет машинное время. Поток может выполнять какую-то часть общего кода процесса, в том числе и ту часть, которая в это время уже выполняется другим потоком. Например, код функции, отображающей на экране степень продвижения процесса передачи информации, может одновременно выполняться двумя потоками, которые обслуживают двух клиентов одного сервера.

    Примечание
    Примечание

    Сравнительно недавно появилось еще несколько терминов, связанных с этой же тематикой. Нитью (fiber) потока называется выполняемый блок кодов, который «вручную» (manually) прерывается или планируется (scheduled) приложением. Нить выполняется в контексте потока, который ее планирует. Заданием (job object) называется группа процессов, объединенных в общий блок (unit). Задание в ОС имеет свое имя, атрибуты защиты и способность управлять общими (разделяемыми) ресурсами. Операции, производимые системой или программистом над заданиями, воздействуют на все составляющие его процессы.
    Все потоки (threads) одного процесса пользуются ресурсами породившего их процесса. Кроме того, каждому потоку система и/или программист приписывает приоритет выполнения и набор структур языка С, описывающих контекст потока. Система использует их для запоминания контекста потока, когда его выполнение прерывается. В контекст входят:

  • состояние регистров;
  • системный стек ядра ОС (kernel stack);
  • стек пользователя (user stack), расположенный в адресном пространстве процесса;
  • блок переменных окружения потока.
  • Потоки подобны процессам, но требуют меньших затрат при своем создании. Они в меньшей степени, чем процессы, защищены друг от друга, но позволяют совместить выполнение операций и выиграть в общей производительности процесса. Перечислим наиболее типичные случаи, когда следует применять мпогопоточность:

  • управление вводом в различные документы МШ-интерфейса. Ввод данных каждого документа приложения организован в виде отдельного потока;
  • управление вводом данных из нескольких устройств телекоммуникации;
  • разграничение приоритетов выполнения задач. Потокам, требующим высокой скорости реакции, присваивается высокий приоритет, а другим потокам более низкий;
  • снижение времени реакции на действия пользователя по вводу данных при одновременном выполнении фоновых вычислений.
  • Обычно более эффективной является реализация многозадачности в виде одного процесса с несколькими потоками, чем в виде многих процессов с одним потоком, так как:

  • контексты потоков занимают меньший объем, чем контексты процессов и система переключает их быстрее;
  • взаимодействие потоков проще, так как они могут пользоваться глобальными переменными в общем для них адресном пространстве процесса;
  • потоки одного процесса легче синхронизировать, так как им доступны описатели объектов ядра из общего контекста процесса.
  • Если один поток выполняет медленные операции ввода-вывода, а другой выполняет вычисления, используя только процессор, то эффективность процесса, совмещающего два потока, будет значительно выше, чем эффективность двух процессов, выполненных последовательно. Типичным многопотоковым приложением является сервер, обслуживающий многих пользователей. Каждый новый пользователь обслуживается отдельным потоком одного процесса. Вместо ожиданий, которые связаны с дисковыми операциями, система может перейти к выполнению другого потока.

    Однако в случае ошибочного проектирования потоки могут и ухудшить общий показатель эффективности процесса. Например, время выполнения процесса с двумя потоками будет ниже, чем эффективность двух последовательных однопо-токовых процессов, если оба потока выполняются в памяти и не требуют интерфейса с пользователем. Система вынуждена постоянно прерывать эффективно работающие потоки и переключаться между ними. Эти переключения ведут к ненужным потерям или лишним операциям (overheads) по загрузке в память и последующей выгрузке структур данных, необходимых для обслуживания потоков. Наличие пользовательского интерфейса приводит к тому, что указанные действия выполняются во время неизбежных пауз, связанных с операциями ввода-вывода, что создает иллюзию совмещения во времени.

    Создание многопотоковых процессов требует тщательного предварительного анализа с тем, чтобы должным образом скоординировать действия операционной системы и других программных компонентов. Отслеживание состояний многочисленных потоков требует значительных временных затрат, поэтому следует помнить, что Win32-API предоставляет и другие средства реализации асинхронное™ выполнения операций. Например: асинхронный ввод-вывод (I/O), специальные порты I/O (completion ports), асинхронные вызовы удаленных процедур (asynchronous procedure calls — АРС), функции ожидания системных событий (wait functions).

    Совместный доступ потоков к разделяемым ресурсам: описателям файлов, портов, глобальным переменным, может создать конфликты. Например, один поток читает данные, а другой пытается одновременно их изменить или один поток ждет завершения определенной операции другим потоком, а тот, в свою очередь, ждет отработки первого потока. Такое зацикливание называется тупиковой ситуацией (deadlock). Для предупреждения конфликтов такого рода существуют специальные синхронизирующие объекты ядра системы:, семафоры, мьютексы, события.

    Разделяемые ресурсы

    Разделяемые ресурсы

    В современном операционном окружении программист не может быть уверен и не должен полагаться на то, что коды его программы будут выполняться в тон же последовательности, в какой они написаны. Выполнение одной из функций программы может быть остановлено системой и возобновлено позднее, причем это может произойти даже при выполнении тела какого-либо цикла. При проектировании многопотоковых приложений следует иметь в виду, что ресурсы, разделяемые потоками (блоки памяти или файлы), можно неосознанно повредить. Чтобы показать, как это происходит, рассмотрим пример, который приведен в книге Jesse Liberty «Beginning Object-Oriented Analysis and Design with C++» (Дж. Либерти «Начало объектно-ориентированного анализа и проектирования с помощью C++»), доступной в MSDN.
    Представьте себе пассажирский авиалайнер в полете, а в нем такой разделяемый всеми ресурс, как туалетная комната. Создатели самолета предполагали, что только одна персона может занимать эту комнату. Первый, кто ее занял, закрывает (lock) доступ к пей для всех остальных. Следующий пассажир, желающий воспользоваться этим ресурсом, может либо терпеливо ожидать освобождения, либо по истечении какого-то времени (time out) вернуться на свое сиденье и продолжать заниматься тем, чем он был занят до этого события. Решение о том, что выбрать и как долго ждать, принимает пассажир. Блокирование ресурса порождает неэффективное проведение времени второго пассажира как ожидающего очереди, так и избравшего другую тактику.
    Возвращаясь к многопотоковым процессам, отметим, что если не блокировать ресурс, то становится возможным повреждение данных. Представьте, что один поток процесса проходит по записям базы данных, повышая зарплату каждому сотруднику на 10%, а другой поток в это же время изменяет почтовые индексы в связи с введением нового стандарта. Согласитесь с тем, что разумно совместить эти две работы в одном процессе с целью повышения производительности. Что может произойти, если не блокировать доступ к записи при ее модификации? Первый поток прочел запись (все ее поля), и занят вычислением повышения (предположим, с $80 000 до $85 000). В это время второй поток читает эту же запись с целью изменения почтового индекса. В этой ситуации может произойти следующее: первый поток сохраняет измененную запись с новым значением зарплаты, а второй, возвращая запись с измененным индексом, реставрирует значение зарплаты и данный сотрудник останется без повышения. Это происходит по причине того, что оба потока не могут обратиться к части записи и поэтому работают со всей записью, хотя модифицируют только отдельные ее поля.

    Разделы адресного пространства процесса

    Рисунок 12.8. Разделы адресного пространства процесса

    Разделы адресного пространства процесса
    Примечание
    Примечание


    В системах Windows NT Server Enterprise Edition и Windows 2000 Advanced Server процессу доступны нижние 3 Гбайт и только 1 Гбайт резервируется системой.
    Любому Wm,32-nponeccy могут понадобиться объекты ядра Windows, а также ее подсистемы User или GDI. Они расположены в динамически подключаемых библиотеках: Kernel32.dll, User32.dll, Gdi32.dll и Advapi32.dll Эти библиотеки при необходимости подгружаются в верхнюю часть блока, доступного процессу.

    Общий объем памяти, который система может предоставить всем одновременно выполняемым процессам, равен сумме физической памяти RAM и свободного пространства па диске, которым может пользоваться специальный страничный файл (paging file). Страницей называется блок памяти (4 Кбайт для платформ х86, MIPS, PowerPC и 8 Кбайт для DEC Alpha), который является дискретным квантом (единицей измерения) при обмене с диском. Виртуальный адрес в пространстве процесса проецируется системой в эту динамическую страничную память с помощью специальной внутренне поддерживаемой структуры данных (page map). Когда система перемещает страницу в страничный файл, она корректирует page тар того процесса, который ее используют. Если системе нужна физическая память RAM, то она перемещает на диск те страницы, которые дольше всего не использовались. Манипуляции с физической памятью никак не затрагивают приложения, которые работают с виртуальными адресами. Они просто не замечают динамики жизни физической памяти.

    Функции API для работы с памятью (virtualAlloc и virtualFree) позволяют процессу получить страницы памяти или возвратить их системе. Процесс отведения памяти имеет несколько ступеней, когда блоки памяти постепенно проходят через определенные состояния. Страницы памяти в виртуальном адресном пространстве процесса могут пребывать в одном из трех возможных состояний.

    Таблица 12.2. Состояния страниц памяти в виртуальном адресном пространстве процесса

    Состояние

    Описание

    Free Страница недоступна, но ее можно либо зарезервировать (reserve) для процесса, либо отдать процессу (committed)
    Reserved Зарезервированный блок памяти недоступен процессу и не связан с какой-либо физической памятью, но он подготовлен для того, чтобы в любое время быть отданным (committed) процессу. Зарезервированный диапазон адресов не может быть отдан другому потоку этого же процесса. Такой способ работы снижает фрагментарность физической памяти, так как обычно память резервируется для какой-либо динамической структуры с учетом ее будущего роста.
    Committed

    Отданная страница представляет интересы уже реальной физической памяти как в RAM, так и на диске. Она может иметь различную степень доступа для процесса. (Readonly, ReadWrite и т. д.)

    Память, которую процесс отводит, вызывая функцию virtualAlloc, доступна только этому процессу. Если какая-то DLL в пространстве процесса отводит себе новую память, то она размещается в пространстве процесса, вызвавшего DLL, и недоступна для других процессов, одновременно пользующихся услугами той же DLL. Иногда необходимо создать блок памяти, который был бы общим для нескольких процессов или DLL, используемых несколькими процессами. Для этой цели существует такой объект ядра системы, как файлы, проецируемые в память (file mapping).

    Два процесса создают два упомянутых объекта с одним и тем же именем, получают описатель (handle) объекта и работают с ним так, как будто этот объект находится в памяти. На самом деле они работают с одними и теми же страницами физической памяти. Заметьте, что эта память не является глобальной, так как она остается недоступной для других процессов. Кроме того, ей могут соответствовать различные виртуальные адреса в пространствах разных процессов, ее разделяющих. Если процессы намерены записывать в общую память, то во избежание накладок вы должны использовать синхронизирующие объекты ядра Windows (семафоры, мыотексы, события).

    Алгоритм работы с динамической памятью процесса довольно сильно отличается от привычного алгоритма работы с динамической памятью области heap в программах на языке C++. Там вы с помощью операции new отводите память определенного размера, работаете с ней и затем освобождаете ее операцией delete. Здесь необходимы более сложные манипуляции:

  • резервирование диапазона адресов в виртуальном пространстве процесса. Физическая память при этом не выделяется;
  • отдача (commiting) процессу какого-то количества страниц из предварительно зарезервированного диапазона адресов. При этом процессу становится доступной физическая память, соответствующая виртуальной. Здесь одновременно указывается тип доступа к выделенным страницам (read-write, read-only, или no access). Сравните с обычным способом, который всегда выделяет страницы С доступом read-write;
  • освобождение диапазона зарезервированных страниц;
  • освобождение диапазона отданных страниц. Здесь освобождается физическая память.
  • Кроме того, возможна операция блокирования страниц памяти в RAM, которая запрещает системе перемещать их в страничный файл подкачки (paging file). Есть функция, позволяющая определить текущее состояние диапазона страниц и изменить тип доступа к ним.

    Разделы адресного пространства

    Разделы адресного пространства

    Ниже на Рисунок 12.8 показано как разбивается память на разделы (partitions) в адресном пространстве процесса под управлением Windows NT. Разделы будем рассматривать, двигаясь сверху вниз, от старших адресов к младшим. Верхнюю половину памяти (от 2 Гбайт до 4 Гбайт) система использует для своих нужд. Сюда она грузит свое ядро (kernel) и драйверы устройств. При попытке обратиться к адресу памяти из этого диапазона возникает исключительная ситуация нарушения доступа и система закрывает приложение. Заметьте, что половину памяти у нас отняли только из-за того, что иначе не удалось добиться совместимости с процессором MIPS R4000, которому нужна память именно из этого раздела.
    Следующий небольшой раздел (64 К) также резервируется системой, но никак ей не используется. При попытке обращения к этой памяти возникает нарушение доступа, но приложение не закрывается. Система просто выдает сообщение об ошибке. Большинство из вас знают, что потеря контроля над указателем в программе на языке С или C++ может привести к ошибкам такого рода. Следующие (почти) 2 Гбайт отданы в собственность процесса. Сюда загружаются исходный код приложения (ехе-модуль), динамические библиотеки (dll), здесь также располагаются стеки потоков и области heap, в которых они черпают динамически выделяемую память. Последний маленький (64 К) раздел, так же как и третий раздел, не используется системой и служит в качестве ловушки «непослушных» (wild) указателей.



    Вытеснение потока с более низким приоритетом

    Рисунок 12.6. Вытеснение потока с более низким приоритетом

    Вытеснение потока с более низким приоритетом
    Примечание
    Примечание

    Термин foreground обозначает то качество процесса, которое характеризует — его с точки зрения связи с активным окном Windows. Foreground window — это окно, которое в данный момент находится в фокусе и, следовательно, расположено поверх остальных. Это состояние может быть получено как программным способом (вызов функции SetFocus), так и аппаратно (пользователь щелкнул окно).
    Плакировщик изменяет класс процесса, связанного с этим окном, так чтобы он был больше или равен классу любого процесса, связанного с background-окном. Класс приоритета вновь восстанавливается при потере процессом статуса foreground. Отметьте, что в Windows NT/2000 пользователь может управлять величиной ускорения всех процессов класса NORMAL_PRIORITY с помощью панели управления (команда System, вкладка Performance, ползунок Boost Application Performance).

    Когда окно получает сообщение типа WM_TIMER, WM_LBUTTONDOWN или WM_KEYDOWN, планировщик также ускоряет (boosts) ноток, владеющий этим окном. Существуют еще ситуации, когда планировщик временно повышает уровень приоритета потока. Довольно часто потоки ожидают возможности обратиться к диску. Когда диск освобождается, блокированный поток просыпается и в этот момент система повышает его уровень приоритета. После ускорения потока планировщик постепенно снижает уровень приоритета до базового значения. Уровень снижается на одну единицу после завершения очередного кванта времени. Иногда система инвертирует приоритеты, чтобы разрешить конфликты типа deadlock. Благодаря динамике изменения приоритетов потоки активного процесса вытесняют потоки фонового процесса, а потоки с низким приоритетом все-таки имеют шанс получить управление.

    Представьте такую ситуацию: поток 1 с высоким приоритетом вынужден ждать, пока поток 2 с низким приоритетом выполняет код в критической секции. В это же время готов к выполнению поток 3 со средним значением приоритета. Он получает время процессора, а два других потока застревают на неопределенное время, так как поток 2 не в состоянии вытеснить поток 3, а поток 1 помнит, что надо ждать, когда поток 2 выйдет из критической секции. Отметьте, что планировщик не способен провести анализ подобной ситуации и решить проблему. Он видит ситуацию только в свете существования двух готовых (ready) потоков с разными приоритетами.

    Windows NT/2000 разрешает эту ситуацию так. Планировщик увеличивает приоритеты готовых потоков на величину, выбранную случайным образом. В нашем примере это приводит к тому, что поток с низким приоритетом получает шанс на время процессора и, в течение, может быть, нескольких квантов закончит выполнение кодов критической секции. Как только это произойдет, поток 1 с высоким приоритетом сразу получит управление и сможет, вытеснив поток 3, начать выполнение кодов критической секции.

    Windows 95 разрешит эту ситуацию по-другому. Она определяет факт того, что поток 1 с высоким приоритетом зависит от потока 2 с низким приоритетом, и повышает приоритет второго потока до величины приоритета первого. Это приводит к тому, что поток 3 вытесняется потоком 2 и он, закончив выполнение кодов критической секции, пропускает вперед ждавший поток 1.

    В системе Windows NT/2000 программист имеет возможность управлять процессом ускорения потоков с помощью API-функций SetProcessPriorityBoost (все потоки данного процесса) или SetThreadPriorityBoost (данный поток) в пределах, которые обозначены на Рисунок 12.7. Функции GetProcessPriorityBoost и GetThreadPriorityBoost позволяют выяснить текущее состояние флага.



    Семафоры

    Семафоры

    Семафором называется объект ядра, который позволяет только одному процессу или одному потоку процесса обратиться к критической секции — блоку кодов, осуществляющему доступ к объекту. Серверы баз данных используют их для защиты разделяемых данных. Классический семафор был создан Dijkstra, который описал его в виде объекта, который обеспечивает выполнение двух операций Р и V. Первая литера является сокращением голландского слова Proberen, что означает тестирование, вторая — обозначает глагол Verhogen, что означает приращивать (increment). Первая операция дает доступ к ресурсу, вторая — запрещает доступ и увеличивает счетчик объектов, его ожидающих. Различают два основных использования семафоров: защита критической секции и обеспечение совместного доступа к ресурсу.
    В качестве примера может служить критическая секция в виде функции, осуществляющей доступ к таблице базы данных.. Другим примером может служить реализация списка операционной системы, который называется process control blocks (PCBs). Это список указателей на активные процессы. В каждый конкретный момент времени только один поток ядра системы может изменять этот список, иначе будет нарушена семантика его использования.
    Семафор может быть использован также для управления перемещением данных (data flow) между п производителями и m потребителями. Существует много систем, имеющих архитектуру типа data flow. В них выход одного блока функциональной схемы целиком поступает на вход другого блока. Когда потребители хотят получить данные, они выполняют операцию типа Р. Когда производители создают данные, они выполняют операцию типа V.
    Традиционно семафоры создавались как глобальные структуры, совместно с глобальными API-функциями, реализующими операции Р и V. Теперь семафоры реализуются в виде класса объектов. Обычно абстрактный класс Семафор определяет чисто виртуальные функции типа Р и V. От него производятся классы, реализующие два указанных типа семафоров: защита критической секции и обеспечение совместного доступа к ресурсу. В смысле видимости оба типа семафоров могут быть объявлены либо глобально во всей операционной системе, либо глобально в пространстве процесса. Первые видны всем процессам системы и, следовательно, могут ими управлять. Вторые действуют только в пространстве одного процесса и, следовательно, могут управлять его потоками.
    Сам семафор ничего не знает о том, что он защищает. Ему обычно передается ссылка на объект класса, который хочет использовать критическую секцию, и он либо дает доступ к объекту, либо подвешивает (suspends) объект до тех пор, пока доступ не станет возможным. Важно отметить, что при реализации семафоров и других объектов ядра используют специальные атомарные команды (atomic steps), которые не прерываются системой.

    В некоторых случаях потоку необходимо

    События

    В некоторых случаях потоку необходимо ждать, пока другие-потоки не завершат выполнение каких-то операций или не произойдет какое-либо событие (UI-событие User Interface), то есть событие, инициированное пользователем. В качестве примера, предположим, что имеется 50 выходных телефонных портов и каждый из них управляется отдельным потоком. Пусть класс ccaller для управления соединениями (звонками) уже разработан. Есть также выделенный поток, который управляет всеми портами и отслеживает их статус. Допустим, что до того, как сделать какой-нибудь звонок (call), надо инициализировать все потоки. Тогда алгоритм ожидания множественного события может выглядеть так, как показано ниже. В рассматриваемом фрагменте предполагается, что объект СЕ vent m_nTotalCallers; уже существует и должным образом инициализирован:

    //======= Цикл по всем портам

    for (int i = 0; i {

    //=== Предварительные установки и создание потоков

    CCaller * pCaller = new CCaller(Лпараметры*/);

    BOOL bRc = pCaller->CreateThread();
    }

    //======= Блокировка

    CSingleLock lock (Sm_CallersReadyEvent) ;

    //======= Попытка дождаться события

    if (lock.Lock(WAIT_VERY_LONG_TIME))
    {

    for (i=0; i
    {

    //===== Совершение соединений (звонков)

    )

    lock.Unlock();
    }

    else // Отказ ждать
    {

    //====== Обработка исключения

    }

    Класс CEvent представляет функциональность синхронизирующего объект ядра (события). Он позволяет одному потоку уведомить (notify) другой поток о том, что произошло событие, которое тот поток, возможно, ждал. Например, поток, копирующий данные в архив, должен быть уведомлен о том, что поступили новые данные. Использование объекта класса CEvent позволяет справиться с этой задачей максимально быстро.
    Существуют два типа объектов: ручной (manual) и автоматический (automatic). Ручной объект начинает сигнализировать, когда будет вызван метод SetEvent. Вызов ResetEvent переводит его в противоположное состояние. Автоматический объект класса CEvent не нуждается в сбросе. Он сам переходит в состояние nonsignaled, и охраняемый код при этом недоступен, когда хотя бы один поток был уведомлен о наступлении события. Объект «событие» (CEvent) тоже используется совместно с объектом блокировка (CSingleLock или CMultiLock).

    Специальные блокировки

    Специальные блокировки

    Для того чтобы записи-фантомы не создавались, надо избегать блокирования отдельных записей. Альтернативой является блокировка всей таблицы. Но это решение приводит к снижению эффективности работы СУБД. Другим выходом является предикатная блокировка (predicate locking). Предикат мы определили в уроке, посвященном библиотеке шаблонов STL. Это функция, которая может принимать только два значения (false, true} или {0,1}. В нашем примере такая блокировка запомнит не только записи, которые существуют в таблице и удовлетворяют критерию, но и все несуществующие записи такого типа, то есть блокируется весь тип записей заданного типа. Поэтому второй поток процесса найдет таблицу закрытой и будет вынужден ждать окончания работы первого.
    Предикатные блокировки хороши, но достаточно накладны. Еще одной альтернативой являются прецизионные блокировки (precision locking). Они не закрывают доступ к записям, но обнаруживают конфликты, когда транзакция пытается прочесть или сохранить записи. Прецизионные блокировки более просты в реализации, но создают повышенный риск тупиковых ситуаций (deadlocks).
    До сих пор мы говорили о блокировках, которые имеют одинаковую гранулярность, то есть размер единицы блокируемых данных: таблица, запись или поле. Обычно запрос хочет иметь доступ ко всей таблице, в то время как изменения вносятся только в отдельные записи или даже только в отдельные поля записи. Разработчики механизмов синхронизации ищут оптимум между двумя взаимно противоположными устремлениями: обеспечить максимальную защиту и использовать минимальное число блокировок.
    Гранулярные блокировки ограничивают транзакции небольшим множеством определенных предикатов, которые образуют дерево. В корне дерева обычно находится предикат, который разрешает или запрещает доступ ко всей базе данных. На следующем уровне может быть предикат, возвращающий доступ к определенному сайту (site) распределенной базы данных. Следующий уровень связан с таблицей и т. д. вплоть до домена или поля. Блокировки, определенные предикатом какого-то уровня, блокируют все объекты, описываемые предикатом следующего уровня. Это свойство принадлежит всем деревьям. В связи с чем может возникнуть новая проблема. Допустим, что одна транзакция заблокировала базу на уровне записи, а другая в это же время блокирует базу на уровне таблицы. При этом первый поток не может ничего сделать, и вынужден ждать, а второй споткнется при попытке изменить запись, блокированную первой. Опять имеем deadlock.
    Это привело к разработке нового, более изощренного типа блокировок — целевые блокировки (intention locks). Их идея состоит в том, что при блокировке обозначается ее цель. Например, блокировка таблицы сообщает, что ее целью являются изменения на уровне записей. В этом случае устраняется возможность тупиковой ситуации рассмотренного выше типа. Например, для установки разделяемой блокировки (share lock) на уровне записей транзакция должна сначала установить целевые блокировки (intention locks) на всех уровнях, которые расположены ниже или выше, в зависимости от интерпретации дерева, то есть на уровне таблицы, на уровне базы данных или сайта, если база является распределенной. После этого можно произвести запрос на становку блокировки типа share-lock на уровне записей.

    Стратегии решения проблемы

    Стратегии решения проблемы

    Для того чтобы исключить подобный сценарий, автор многопотокового приложения должен решать проблему синхронизации при попытке одновременного доступа к разделяемым ресурсам. Если говорить о файлах с совместным доступом, то сходная ситуация может возникнуть и при столкновении различных процессов, а не только потоков одного процесса. Разработчика в этом случае уже не устроит стандартный способ открытия файла. Например1:

    //======= Создаем объект класса CFile

    CFile file;

    // ====== Строка с именем файла

    CString fn("MyFile.dat");

    //===== Попытка открыть файл для чтения

    if ( ! file.Open(fn,CFile::modeRead))

    {

    MessageBox ("He могу открыть файл "+fn, "Ошибка");

    return;
    }

    Он должен писать код с учетом того, что файл может быть заблокирован какое-то время другим процессом. Если следовать уже рассмотренной тактике ожидания ресурса в течение какого-то времени, то надо создать код вида:

    bool CMyWnd::TryOpen()

    <

    //====== Попытка открыть файл и внести изменения

    CFile file;

    CString fn("MyFile.dat"), Buffer;

    //===== Флаг первой попытки
    static bool bFirst = true;

    if (file.Open (fn, CFile:: modeReadWrite I CFile::shareExclusive))

    {

    // Никакая другая программа не сможет открыть

    // этот файл, пока мы с ним работаем

    int nBytes = flie.Read(Buffer,MAX_BYTES);

    //==== Работаем с данными из строки Buffer
    //==== Изменяем их нужным нам образом

    //==== Пришло время вновь сохранить данные
    file.Write(Buffer, nBytes);

    file. Close ();

    //==== Начиная с этого момента, файл доступен
    //==== для других процессов
    //==== Если файл был открыт не с первой попытки,
    //==== то выключаем таймер ожидания
    if (IbFirst)

    KillTimer(WAIT_ID);

    //===== Возвращаем флаг успеха
    return bFirst = true;

    }

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

    else

    if (bFirst) // и эта неудача — первая,
    //===== то запускаем таймер ожидания
    SetTiraer(WAIT_ID, 1000, 0);

    //===== Возвращаем флаг неудачи

    return bFirst = false;
    }

    В другой функции, реагирующей на сообщения таймера, называемой, как вы знаете, функцией-обработчиком (Message Handler), надо предусмотреть ветвь для реализации выбранной тактики ожидания:

    //====== Обработка сообщений таймера

    void CMyWnd::OnTimer(UINT nID)
    {

    //====== Счетчик попыток

    static int iTrial = 0;

    //====== Переход по идентификатору таймера

    switch (nID)

    {

    //== Здесь могут быть ветви обработки других таймеров

    case WAIT_ID:

    //====== Если не удалось открыть

    if (ITryOpenO)

    {

    //===== и запас терпения не иссяк,

    if (++iTrial < 10)

    return; // то продолжаем ждать

    //=== Если иссяк, то сообщаем о полной неудаче
    else
    {

    MessageBox ("Файл занят более 10 секунд",

    "Ошибка"); //====== Отказываемся ждать

    KillTimer(WAIT_ID);

    //====== Обновляем запас терпения

    iTrial = 0;
    }
    }
    }
    }

    Существуют многочисленные варианты рассмотренной проблемы, и в любом случае программист должен решать их, например путем синхронизации доступа к разделяемым ресурсам. Большинство коммерческих систем управления базами данных умеют заботиться о целостности своих данных, но и вы должны обеспечить целостность данных своего приложения. Здесь существуют две крайности: отсутствие защиты или ее слабость и избыток защиты. Вторая крайность может создать низкую эффективность приложения, замедлив его работу так, что им невозможно будет пользоваться. Например, если в примере с повышением зарплаты первый поток заблокирует-таки доступ к записи, но затем начинает вычислять новое значение зарплаты, обратившись к источнику данных о средней (в отрасли) зарплате по всей стране. Такое решение проблемы может привести к ситуации, когда второй поток процесса, который готов корректировать эту же запись, будет вынужден ждать десятки минут.

    Одним из более эффективных решений может быть такое: первый поток читает запись, вычисляет прибавку и только после этого блокирует, изменяет и освобождает запись. Такое решение может снизить время блокирования до нескольких миллисекунд. Однако защита данных теперь не сработает в случае, если другой поток поступает также. Второй поток может прочитать запись после того, как ее прочел первый, но до того, как первый начал изменять запись. Как поступить в этом случае? Можно, например, ввести механизм слежения за доступом к записи, и если к записи было обращение в интервале между чтением и модификацией, то отказаться от модификации и повторить всю процедуру вновь.

    Примечание
    Примечание

    Каждое решение создает новые проблемы, а поиск оптимального баланса ложится на плечи программиста, делая его труд еще более интересным. Кстати, последнее решение может вызвать ситуацию, сходную с той, когда два человека уступают друг другу дорогу. Отметьте, что решение вопроса кроется в балансе между производительностью (performance) и целостностью данных (data integrity).

    Транзакции

    Транзакции

    В системе с преимущественной многозадачностью ноток может быть прерван в любой момент. Обычно перед выполнением очередной машинной команды система смотрит, есть ли прерывание. Если есть и приоритет его достаточно высок, то текущая команда не выполняется, а система переходит в режим обработки прерывания. Если программа написана без учета этого обстоятельства (not thread-safe), то последствия могут быть неожиданными. Например, если поток проверяет значение какого-то глобального флага и в зависимости от его значения выполняет разветвление, то возможна ошибка из-за того, что флаг мог быть изменен другим потоком, перехватившим управление в промежутке между этими двумя командами. Для создания thread-safe-прмложешт программист должен уметь синхронизировать доступ к критическим объектам так, чтобы один поток не портил работу другого.
    Важным понятием, которое имеет отношение к рассматриваемой проблеме и используется при описании алгоритмов управления источниками данных, является транзакция. Представьте, что один клиент банка производит депозит чека, который получен им от другого клиента. При выполнении этой операции нормально функционирующая система должна либо изменить оба счета, либо оставить все без изменений. Если на счету клиента, выписавшего чек, есть указанная сумма, то этот счет уменьшается, а счет второго клиента увеличивается. Если указанной суммы на счету первого клиента не оказалось, то оба счета должны остаться без изменений. Идея транзакции заключается в том, что после проведения изменений в записях базы данных все они либо принимаются (committed), либо отвергаются (rolled-back или aborted).
    Транзакция — это множество операций, которые выполняются либо все, либо ни одна. Транзакция представляет собой последовательность операций над БД (базой данных), рассматриваемых СУБД как единое целое и необходимых для поддержания ее логической целостности. То свойство, что каждая транзакция начинается при целостном состоянии БД и оставляет это состояние целостным после своего завершения, делает очень удобным использование этого понятия как единицы активности пользователя по отношению к БД. Для поддержки многозадачности требуются следующие механизмы обработки данных:
  • начало транзакции, => изменение данных, => принятие изменений (commit);
  • начало транзакции, => изменение данных, => отмена транзакции (roll back);
  • восстановление системы после программного или аппаратного сбоя;
  • восстановление системы после потери данных на диске;
  • архивирование базы данных;
  • создание контрольных точек (checkpoints) — копий текущих состояний для возможности их восстановления.
  • Одним из основных требований к СУБД является надежность хранения данных во внешней памяти. Под надежностью хранения понимается то, что СУБД должна быть в состоянии восстановить последнее согласованное состояние БД после любого аппаратного или программного сбоя. Обычно рассматриваются два возможных вида аппаратных сбоев: так называемые мягкие сбои, которые можно трактовать как внезапную остановку работы компьютера, например аварийное выключение питания, и жесткие сбои, характеризуемые потерей информации на носителях внешней памяти. Примерами программных сбоев могут быть:

  • аварийное завершение работы СУБД по причине ошибки в программе или в результате некоторого аппаратного сбоя,
  • аварийное завершение пользовательской программы, в результате чего некоторая транзакция остается незавершенной.
  • Во всех случаях придерживаются стратегии «упреждающей» записи в журнал, так называемого протокола WAL — Write Ahead Log. Она заключается в том, что запись об изменении любого объекта БД должна попасть во внешнюю память журнала раньше, чем измененный объект попадет во внешнюю память основной части БД. Известно, что если в СУБД корректно соблюдается протокол WAL, то с помощью журнала можно решить все проблемы восстановления БД после любого сбоя.

    Тупиковая ситуация (Deadlock)

    Тупиковая ситуация (Deadlock)

    Выше мы рассмотрели, как потоки одного процесса могут вступить в конфликт и испортить работу друг друга. Одной из задач программиста является обеспечить невозможность такого сценария. Другими возможными неприятностями могут быть: рассинхронизация (race conditions) и тупиковая ситуация (deadlock).
    Первая может произойти, когда успех одной операции зависит от успеха другой, но обе они не синхронизированы друг с другом. Предположим, что один поток процесса подготавливает принтер, а другой ставит задание на печать (print job) в очередь. Если потоки не синхронизированы и первый из них не успеет выполнить свою работу до того, как начнется печать, то мы получим сбой.
    Примечание
    Примечание

    Но в каком-то количестве случаев все пройдет гладко. Такой тип ошибок очень неприятен, так как в процессе отладки ее нельзя уверенно и многократно воспроизводить. Рассинхронизация порождает ненадежность —тип ошибок, который большинство программистов всего мира ненавидит. В MSDN, но, к сожалению, не в литературе, вы часто можете встретить упоминания о коварстве irreprodudble bugs (невоспроизводимые ошибки). Суверенностью можно сказать, что книга под названием «Технологии борьбы с ошибками» была бы бестселлером.
    Тупиковая ситуация создается, когда один поток ждет завершения второго, а второй ждет завершения первого. Представьте, что один поток реализует такую функцию:

  • блокирует запись, идентифицирующую клиента;
  • блокирует запись, описывающую его счет;
  • изменяет обе записи;
  • освобождает запись, описывающую счет;
  • освобождает запись, идентифицирующую клиента.
  • Обратите внимание на то, что освобождение блокировок происходит в обратном порядке. Именно так следует поступать при работе с записями базы данных и всеми объектами ядра Windows. Предположим далее, что второй поток реализует функцию начисления месячного процента и он делает те же действия, что и первый, но порядок блокирования и освобождения записей обратный. Оба потока по отдельности функционируют вполне надежно. В процессе работы возможен следующий сценарий: первый поток блокирует запись, идентифицирующую клиента, затем второй блокирует запись, описывающую его счет. После этого оба ждут освобождения записей, блокированных друг другом. Если ожидание реализовано разработчиком в виде бесконечного цикла, то мы его получили. Это тупиковая ситуация, или deadlock.

    Уровни и платформы

    Уровни и платформы

    В последнее время в компьютерном мире обрел популярность термин layer (уровень или слой). Так, различают аппаратный уровень (hardware layer), устойчивый программный уровень (firmware layer) и просто программный уровень (software layer).

    Примечание
    Примечание

    Термин firmware обозначает системные процедуры, хранимые в постоянной памяти (ROM) и поэтому не разрушаемые при выключении основного питания. Там обычно хранятся инициализирующие (startup) процедуры, а также низкоуровневые команды ввода-вывода. В смысле простоты внесения изменений уровень firmware занимает промежуточное положение между hardware (аппаратные средства) и software (программное обеспечение).
    Наряду с этим выделяют другие уровни, например уровень операционной системы, уровень прикладной программы или приложения. Базовый или нижний уровень описания компьютера называется платформой. В общеупотребительном смысле этот термин обозначает тип используемого процессора и/или операционной системы. Любая программа, будь то ваша прикладная, компилятор языка C++, операционная система или драйвер устройства — специальная программа, помогающая управлять каким-либо устройством, в конечном счете представляет собой последовательность машинных инструкций или команд процессора. Команды реализованы на аппаратном уровне (hardware). Все разновидности процессоров обладают своей собственной системой команд, которая совместно с архитектурой процессора и операционной системой, в сущности, и образует платформу. Вы знаете, что есть платформы: DEC Alpha, PowerPC (RISC-платформы), MIPS, Macintosh, Intel (x86) и др. Кроме того, говорят о DOS-платформе или платформе Win32. Здесь уже имеют в виду одного производителя операционной системы, но подчеркивают различие в ее архитектуре, а также длине машинного слова (16 или 32 бита).

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

  • человеком-оператором;
  • прикладной программой;
  • ресурсами вычислительной системы.


  • Устранение тупиковых ситуаций

    Устранение тупиковых ситуаций

    Единственным способом выхода из тупиковой ситуации (deadlock) является снятие блокировки одним из потоков. Это означает прерывание или отказ от транзакции. Система может либо предупреждать захваты либо допускать их, но затем соответствующим образом обрабатывать. Оказалось, что стандартные протоколы работы с базами данных достаточно редко приводят к образованию захватов, поэтому было признано целесообразным допускать появление захватов при условии, что разработаны механизмы их обнаружения. Особую проблему составляет обнаружение захватов в распределенных базах данных, так как нет простого способа, с помощью которого один узел сети может узнать, какие блокировки наложены в данный момент другим узлом.
    Два технических приема используются для обнаружения тупиковой ситуации. Первый — это установка таймера перед совершением транзакции. Если время для ее совершения вышло, то транзакция прерывается, блокировка снимается, что дает возможность другому потоку или процессу закончить свою операцию. Это решение очень просто реализовать, но его недостатком является то, что врожденно медленные транзакции могут потерять шанс быть выполненными. Другим методом является создание специальной структуры данных, которая моделирует граф ожиданий (waits-for graph) — бинарное отношение ожидания между транзакциями. Узлами графа являются транзакции, а дугами — факты ожидания. Так, например, дуга (i, j) (из узла i в узел j) существует, если транзакция i ожидает освобождения блокировки, наложенной транзакцией j. Очевидно, что тупиковой ситуации в этой модели соответствует цикл. Отметьте, что" длина цикла (количество его дуг) может быть более двух.
    Алгоритмы обнаружения циклов в графах давно разработаны. Графы обычно хранятся в виде динамических списков, то есть каждый узел хранит список блокировок — указателей на транзакции, которые ему мешают. Сам список обычно защищен семафором. С целью экономии времени процессора детектор циклов включается лишь при необходимости или периодически. Цикл считается обнаруженным, если в списке транзакций, которые тормозят данную, присутствует транзакция, в списке которой есть указатель на исходную. Эту фразу, вероятно, придется прочесть несколько раз.
    Отметим, что библиотека классов MFC поддерживает механизмы синхронизации, но детали их реализации скрыты от разработчика. Тем не менее он может использовать их, не заботясь о деталях реализации. Главным требованием при этом, как и при работе с любыми другими объектами классов MFC, является соблюдение протокола, описанного в интерфейсе класса.
    К сожалению, время, отведенное для написания книги, закончилось и мне не удастся привести и описать примеры приложений, иллюстрирующих использование синхронизирующих объектов ядра Windows, хотя такие примеры разработаны и достаточно давно используются в вышеупомянутом учебном центре.

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

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

    Приложения и защищенные подсистемы взаимодействуют по типу клиент-сервер. Приложения (клиенты) запрашивают подсистемы (серверы) о необходимости выполнить какой-то сервис. При этом клиенты и серверы общаются посредством строго определенной последовательности сообщений. Такой стиль называется Inter-Process Communications (IPC — Обмен данными между процессами), и он имеет форму либо местных вызовов процедур Local Procedure Call (LPC), либо удаленных вызовов — Remote Procedure Call (RPC). Если и клиент, и сервер расположены в одном компьютере, TO Windows NT Executive использует LPC — оптимизированную разновидность общепринятого стандарта RPC, который действует между клиентами и серверами, расположенными в пределах одной сети компьютеров. Стандарт RPC позволяет обмениваться услугами с серверами, работающими на других платформах, например из UNIX-окружения.

    Windows 2000 — многозадачная операционная система

    Windows 2000 — многозадачная операционная система
    В тексте этого урока использованы материалы, любезно предоставленные преподавателем СПбГТУ Мариной Полубенцевой, с которой мы совместно ведем курс Visual C++ в Microsoft Certified Educational Center при Санкт-Петербургском государственном техническом университете (www.Avalon.ru).

    Разработчики Windows-приложений живут в особом мире событий и сообщений, в котором последовательность выполнения операций не всегда строго предсказуема. Они выработали свое особое представление о том, как правильно ставить и решать задачи в виртуальном мире операционной системы, управляемой событиями. Если вы, читатель, выполнили все шаги по разработке традиционного Windows-приложения, описанные в третьем уроке этой книги, то, вероятно, уже имеете понятие о структуре и принципе функционирования любой Windows-программы. Традиционным называется приложение, созданное на основе функций API (Application Programming Interface) или программируемого интерфейса приложений. API — это подсистема Windows, которая помогает программировать, то есть планировать и создавать, графический интерфейс пользователя. В состав API, как вы знаете, входят не только функции, но и множество структур языка С, сообщений Windows, макросов и интерфейсов.

    

        Биржевая торговля: Механические торговые системы - Создание - Программирование