Учебник по Visual C++ .Net

         

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


CTreeApp : public CWinApp

{

public:

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

CMultiDocTemplate *m_pTemplDraw; CMultiDocTemplate *m_pTemplTree;

CTreeApp () ;

virtual BOOL Initlnstance();

afx_msg void OnAppAbout();

DECLARE_MESSAGE_MAP()

};



Класс CPolygon


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

typedef vector<CDPoint, allocator<CDPoint>

> VECPTS;

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

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

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

#include <vector> 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<CPolygon, allocator<CPolygon> > 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; // Пока не знаем обратного адреса

MakeStarO; // Зададим полигон в виде звезды

}


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


"DrawView.h"

Внесите изменения в интерфейс класса, так чтобы он стал:

#pragma once

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

class CDrawView : public CView {

DECLARE_DYNCREATE(CDrawView) protected:

CSize m_szView; // Размеры клиетской области окна

bool m_bNewPoints; // Флаг режима вставки новых точек

bool m_bReady; // Флаг готовности захвата вершины

bool m_bLock; // Флаг захвата вершины

int m_CurID; // Индекс полигона в массиве

HCURSOR m_hGrab; // Курсор захвата

CPen m_penLine; // Перо для изображения контура

CDrawView();

virtual ~CDrawView();

public:

CTreeDoc* GetDocument()

{

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


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


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

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

CTreeDoc* GetDocument()

{

return dynamic_cast<CTret=Doc*> (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<nRows; i++)

{

//====== Цикл прохода по столбцам

for (int j=0; j<nCols && rKnPoly; j++, n++)

{

//====== Создаем класс окна картинки

CWndGeora *pWnd = new CWndGeom(this, n);

//====== Запоминаем его в контейнере

m_pWnds.push_back(pWnd);

//====== Создаем Windows-окно

pWnd->Create (m_WndClass, 0, style, r, this, 0);

//====== Сдвигаем позицию окна вправо

r.OffsetRect (dx, 0);

}

//=== Начинаем новый ряд картинок (сдвиг влево-вниз)

r.OffsetRect (-nCols*dx, dy);

}

}

Существенным моментом в алгоритме является то, что размер прокручиваемого окна (m_szScroll) зависит от количества картинок. Поэтому сколько бы их не было в текущей папке — все будут доступны с помощью полос прокрутки. Расположение и размеры картинок определяются с помощью объекта класса CRect. Метод Of f setRect этого класса позволяет сдвигать прямоугольник окна в нужном нам направлении.

Обслуживание контейнера m_pWnds дочерних окон типа cwndGeom сопряжено с необходимостью следить за освобождением памяти, занимаемой окнами, в те моменты, когда происходит переход от папки к папке в окне CLef tview. Для этой цели служит вспомогательная функция Clear, которую надо вызывать как в отмеченные выше моменты, так и при закрытии окна. Последний случай сопровождается автоматическим вызовом деструктора класса CRightview. С учетом сказанного введите такие добавки в файл RightView.cpp:

void CRightview::Clear()

{

//====== Цикл прохода по всем адресам контейнера

for (UINT i=0; Km_pWnds. size () ; i++)

{

//====== Уничтожение Windows-окна

m_pWnds[i]->DestroyWindow();

// Освобождение памяти, занимаемой объектом

delete m_pWnds[ i ] ;

}

//===== Освобождение памяти, занимаемой контейнером m_pWnds.clear();

}

//===== Деструктор класса вызывает

Clear CRightview::~CRightview()

{

Clear () ;

}


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


CPolygonS poly){

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

m_pDoc = poly.m_pDoc;

m_nPenWidth = poly.m_nPenWidth;

m_PenColor = poly.m_PenColor;

m_BrushColor = poly.m_BrushColor;

m_ptLT = poly.m_ptLT;

m_ptRB = poly.m_ptRB;

//===== Освобождаем контейнер точек

if (!m_Points.empty()) m_Points.clear();

//====== Копируем все точки. Возможно решение с помощью assign.

for (OINT i=0; i<poly.m_Points.size();

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<n; 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_Points . size 0 ;

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<CTreeDoc*>(m_pDocument);

}

Замены такого рода, когда в h-файл вставляется код, а не только декларации, сопряжены с некоторыми неожиданными сообщениями со стороны компилятора. Здесь важно проявить терпение и не опускать руки раньше времени. Если вы правильно сделали замены, то после компиляции проекта получите предупреждение и сообщение об ошибке. С предупреждением справиться просто, если посмотреть справку по его коду (С4541). Выяснится, что для использования информации о типе указателей на этапе выполнения (run-time type information, которой пользуется выражение dynamic_cast<type-id>(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<CTreeDoc*>(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<CTreeDoc*>(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.

Рис. 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 и создайте функцию для его обработки, выбрав <Add> в ячейке справа.

Найдите заготовку тела функции ctlColor в файле PolyDlg.cpp и вставьте в нее следующие коды:

HBRUSH CClrEdit::CtlColor(CDC* pDC, UINT nCtlColor)

{

pDC->SetTextColor (m_clrText); // Цвет текста

pDC->SetBkColor (m_clrBk); // Цвет подложки текста

return m_brBk; // Возвращаем кисть

}

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

void CClrEdit::ChangeColor(COLORREF clr)

{

//====== Цвет текста - инвертирований цвет фона

m_clrText = ~clr & Oxffffff;

m_clrBk = clr;

//====== Создаем кисть цвета фона

m_brBk.DeleteObject();

m_brBk.CreateSolidBrush (clr);

Invalidate ();

}

Главным управляемым параметром является кисть (m_brBk), которую в ответ на отраженное сообщение =WM_CTLCOLOR надо возвратить каркасу приложения. Попутно мы изменяем цвет текста (setTextColor) и его подложки (setBkColor). Чтобы понять, что такое подложка текста, при отладке временно закомментируйте строку

pDC->SetBkColor (m_clrBk);

При изменении (инвертировании) цвета текста мы вынуждены обнулять четвертый байт переменной m_clrText. В более старых версиях Windows это действие было лишним. Теперь четвертый байт используется для задания степени прозрачности при воспризведении растровых изображений. Если он не равен нулю, то инвертирование цвета не проходит. Первые три байта, как вы помните, задают три компонента (red, green, blue).

Изменение цвета пользователем с помощью элементов управления будет мгновенно отслеживаться в четырех полях диалога (три компонента цвета и суммарный цвет в окне Color). Так как мы хотим отследить изменение цвета и в окне представления, управляемого классом CDrawView, то мы добываем адрес родительского oкna.(GetParent) и вызываем вспомогательную функцию UpdateDrawView.


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


CPolyDlg::OnClickedOk(void)

{

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

m_pDoc->m_pPolyDlg = 0;

//====== Родительская версия вызовет DestroyWindow

CDialog::OnOK();

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

delete this;

}

Вызов диалога производится в ответ на команду Edit > Poly Color, которая уже присутствует в меню IDR_DrawTYPE. Введите в класс CTreeDoc обработчик этой команды и наполните его кодами, как показано ниже:

void CTreeDoc::OnEditPolycolor(void)

{

//====== Если диалог отсутствует

if (!m_pPolyDlg)

{

//====== Создаем его в две ступени

m_pPolyDlg = new CPolyDlg(this);

m_pPolyDlg->Create(IDD_POLYCOLOR) ;

}

else

//===== Иначе делаем активным его окно

m_pPolyDlg->SetActiveWindow();

}

Здесь использован указатель на объект диалогового класса, который необходимо ввести в число public-данных класса CTreeDoc (CPolyDlg *m_pPolyDlg;) и обнулить в конструкторе документа (m_pPolyDlg = 0;). Сделайте это, а также введите в файл реализации класса CTreeDoc вспомогательную функцию UpdateDrawView:

void CTreeDoc::UpdateDrawView()

{

//====== Добываем адрес нужного представления

CDrawView *pView = dynamic_cast<CDrawView*>

(GetView(SCDrawView::classCDrawView));

//====== и просим его перерисоваться с учетом изменений

if (pView)

pView->Invalidate();

}

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

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

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



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


);

m_cPent.Autoload (IDC_PENT, this);

m_cStar.AutoLoad (IDC_STAR, this);

CDialog::OnlnitDialog{);

//====== Установка диапазона ползунков

m_rSlider.SetRange (0, 255);

m_gSlider.SetRange (0, 255);

m_bSlider.SetRange (0, 255);

//====== Установка цены деления ползунков

m_rSlider.SetTicFreq (50);

m_gSlider.SetTicFreq (50);

m_bSlider.SetTicFreq (50);

//=== Вызов обработчиков для начальной

//=== закраски окон и установки ползунков OnChangeRedO ;

OnChangeGreen();

OnChangeBlue ();

return TRUE;

}

В Visual Studio 6 эта функция создавалась как обработчик сообщения WM_INITDIALOG, здесь в Studio.Net 7.0 я не обнаружил сообщения с таким именем в списке сообщений диалогового класса. Однако в списке Overrides присутствует строка с именем OnlnitDialog. В принципе рассматриваемая функция и в Visual Studio 6 имеет прототип virtual BOOL OnlnitDialog, но classWizard 6-й версии причисляет ее к функциям-обработчикам сообщений. Характерным моментом является также то, что прототип функции в Studio.Net изменился и стал BOOL OnlnitDialog (void);. Возвращаясь к диалоговому классу, заметим, что обращение к методам класса CTreeDoc требует включить традиционную строку

#include "TreeDoc.h"

в список директив препроцессора файла PolyDlg.cpp.



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


CPolyDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)

{

//====== Неинтересное для нас сообщение

if (nSBCode==SB_ENDSCROLL)

return;

//====== Устанавливаем флаг сообщений от ползунков

m_bScroll = true;

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

switch(GetFocus()->GetDlgCtrlID())

{

case IDC_RSLIDER:

//====== Считываем текущую позицию движка

m_nRed = m_rSlider.GetPos();

//====== Синхронизируем поле, редактирования

SetDlgltemlnt(IDC_RED, m_nRed);

break;

case IDC_GSLIDER:

m_nGreen = m_gSlider.GetPos();

SetDlgltemlnt(IDC_GREEN, m_nGreen);

break; case IDC_BSLIDER:

m_nBlue = m_bSlider.GetPos() ;

SetDlgltemlnt(IDC_BLUE, m_nBlue);

break;

}

//====== Снимаем флаг сообщений от ползунков

m_bScroll = false;

}

Сообщения от всех ползунков обрабатываются в одной функции. Идентификатор активного ползунка определяется путем последовательного вызова двух функций GetFocus и GetDlgctrliD, являющихся методами класса cwnd.

Флаг сообщений от ползунков (m_bScroll) понадобился нам для того, чтобы при синхронизации элементов управления не происходили повторные вызовы функций-обработчиков. Дело в том, что при изменении позиции ползунка мы должны привести в соответствие окно редактирования, а при ручном изменении числа в окне редактирования мы должны синхронизировать позицию ползунка. Но сообщение EN_CHANGE посылается как при ручном изменении, так и при программном изменении с помощью функции SetDlgltemlnt. Проследим цепь таких событий: пользователь подвинул движок ползунка, мы вызываем SetDlgltemlnt, она провоцирует посылку сообщения EN_CHANGE, а обработчик этого сообщения корректирует положение ползунка, которое и без того верно.

Введите в класс диалога реакции на уведомления EN_CHANGE от четырех элементов IDC_PEN, IDC_RED, IDC_GREEN И IDC_BLUE. Вы помните, что это надо делать с помощью кнопки Events в окне Properties. Вставьте коды в остовы функций обработки, как показано ниже:

void CPolyDlg::OnChangePen(void)

{


BOOL bSuccess; //====== Попытка преобразовать в число

UINT nSize = GetDlgltemlnt(IDC_PEN, SbSuccess, FALSE);

if (bSuccess && nSize < 101)

{

m_nPen = nSize;

m_pDoc->m_Poly-m_nPenWidth = m_nPen;

m_pDoc->UpdateDrawView();

}

}

Отметьте, что здесь мы намеренно не пользуемся функцией UpdateData, которая провоцирует обмен данными сразу со всеми полями окна диалога, так как хотим показать более экономный способ выборочного (целевого) обмена с помощью функции GetDlgltemlnt. Правда, при таком подходе не работают функции проверки данных типа DDV_ и приходится производить проверку самостоятельно:

void CPolyDlg::OnChangeRed(void) {

//====== Если сообщение спровоцировано ползунком,

//====== то обходим коды его синхронизации

if (!m_bScroll)

{

m_nRed = GetDlgltemlnt(IDC_RED, 0, FALSE);

m_rSlider.SetPos(m_nRed);

//====== Изменяем цвет фона окна редактирования

m_cRed.ChangeColor(RGB(m_nRed, 0, 0));

//====== Корректируем интегральный цвет

UpdateColor();

}

void CPolyDlg::OnChangeGreen(void)

{

if (!m_bScroll)

{

m_nGreen = GetDlgltemlnt(IDC_GREEN, 0, FALSE), m gSlider.SetPos(m_nGreen);

m_cGreen.ChangeColor(RGB(0, m_nGreen, 0)); UpdateColor ();

}

void CPolyDlg::OnChangeBlue(void)

{

if (!m_bScroll)

{

m_nBlue = GetDlglteralnt(IDC_BLUE, 0, FALSE);

m_bSlider.SetPos(m_nBlue);

}

m_cBlue.ChangeColor(RGB(0, 0, m_nBlue));

UpdateColor ();

}

Введите тело вспомогательной функции, которая вычисляет интегральный цвет и вносит изменения, перекрашивая окно диалога IDC_COLOR, и с помощью документа текущий полигон в окне CDrawView:

void CPolyDlg::UpdateColor()

{

COLORREF clr = RGB (m_riRed,m_nGreen,m_nBlue) ;

m_cColor.ChangeColor(clr) ;

m_pDoc->m_Poly.m_BrushColor = clr;

m_pDoc->UpdateDrawView();

}

С помощью Studio.Net введите в класс диалога реакции на уведомляющие сообщения (BN_CLICKED) о нажатии кнопок выбора стандартных геометрий для полигонов (IDCJTRI, IDC_PENT и IDC_STAR). В них мы с помощью техники обратного указателя вновь обращаемся к документу и используем его данные и методы для замены координат точек текущего полигона:



void CPolyDlg::OnClickedTri(void)

{

m_pDoc->m_Poly.MakeTria() ;

m_pDoc->UpdateDrawView() ;

}

void CPolyDlg::OnClickedPent(void)

{

m_pDoc->m_Poly.MakePent() ;

m_pDoc->UpdateDrawView() ;

}

void CPolyDlg::OnClickedStar(void)

{

m_pDoc->m_Poly.MakeStar() ;

m_pDoc->UpdateDrawView();

}

Измените тело конструктора диалогового класса, с тем чтобы при открытии диалога он смог запомнить обратный указатель (адрес документа) и все его элементы были правильно инициализированы:

CPolyDlg::CPolyDlg(CTreeDoc* p)

: CDialog (CPolyDlg::IDD, 0)

{

m_pDoc = p;

m_nPen = p->m_Poly.m_nPenWidth;

//====== Расщепляем цвет фона текущего полигона

COLORREF brush = p->m_Poly.m_BrushColor;

m_nRed = GetRValue(brush); // на три компонента

m_nGreen = GetGValue(brush);

m_nBlue = GetBValue(brush) ;

m_bScroll = false; // Ползунки в покое


Обращаемся к операционной системе


Теперь, когда вы научились управлять формой дерева, мы продолжим развитие приложения. Используя клавишу Delete, удалите все ресурсы типа Bitmap. Удалите также глобальное объявление структуры TVINSERTSTRUCT. Теперь мы покажем, что можно обходиться и без ее помощи. Уберите весь учебный код, следующий после строки m_plmgList = new CImageList, и вставьте новый, так, чтобы функция приобрела вид:

void CLeftView::OnInitialUpdate()

{

CTreeView::OnInitialUpdate();

::SetWindowLongPtr(m_Tree.m_hWnd, GWL_STYLE, GetWindowLong(m_Tree.m_hWnd, GWL_STYLE)|TVS_HASLINES I TVS_HASBUTTONSITVS_LINESATROOT|TVS_SHOWSELALWAYS);

//====== Создаем новый список изображений

m_pImgList = new CImageList;

//====== Связываем его с системным списком изображений

GetSvsImqList () ;

//====== Получаем имена логических дисков

char s [1024] ;

DWORD size = ::GetLogicalDriveStrings (1024, s);

if (Isize) // В случае отказа

return; // уходим молча

//=== Сканируем текст и вставляем новые узлы дерева

for (char *pName = s; *pNarae; pName += strlen(pName)+1)

Addltem (TVI_ROOT, pName);

}

Функция GetSysimgList, которую мы создадим чуть позже, получает от системы список системных значков и связывает его с деревом. Начать показ файлового дерева мы решили с демонстрации всех логических дисков, имеющихся в операционной системе в данный момент. API-функция GetLogicalDriveStrings заполняет строку текста, в которую она помещает перечень всех присутствующих в операционной системе логических дисков. Строка имеет особый формат: она состоит из нескольких подстрок, завершающихся нулем, например:

a:\0c:\0d:\00

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


Теперь займемся созданием вспомогательных функций, которые понадобились при разработке функции OninitialUpdate. Введите в файл LeftView.cpp реализацию функции GetSysimgList, объявление которой уже существует в файле интерфейса LeftView.h класса CLef tview:

void CLeftView::GetSysImgList()

{

SHFILEINFO info;

// Попытка получить описатель системного списка значков

HIMAGELIST hlmg = (HIMAGELIST)

::SHGetFilelnfо("С:\\",0, Sinfo,sizeof (info), SHGFI_SYSICONINDEX | SHGFI_SMALLICON);

//=== Приписываем описатель системного списка

//=== изображений объекту CImageList

if (Ihlmg !m_pImgList->Attach(hlmg))

{

MessageBox(0,"He могу получить System Image List!");

return; }

//=== Связывание списка с элементом управления деревом m_Tree.SetlmageList(m_pImgList, TVSIL_NORMAL);

}

Функция SHGetFilelnfo позволяет получить информацию о каком-либо объекте файловой системы. Последний параметр уточняет смысл вопроса. Определяем его с помощью битовых констант SHGFI_SYSICONINDEX и SHGFI_SMALLICON, которые означают, что мы интересуемся индексами значков в системном списке и нам нужны маленькие значки. Вы помните, что Windows поддерживает значки двух типов: большие (32x32) и маленькие (16x16). Результатом вызова функции будет описатель (handle) всего списка значков, который мы затем должны связать с элементом m_Tree. Но сначала требуется прикрепить (attach) Windows-описатель списка к объекту класса CimageList, адрес которого мы храним в переменной m_pImgList.

Понятие прикрепить описатель (attach a handle) вы будете встречать достаточно часто, программируя в рамках MFC, но значительно реже, чем разработчики, базирующиеся на платформе SDK (Software Development Kit), которые не пользуются классами MFC. Вместо этого они используют многочисленные структуры и прямо вызывают функции API из программы на языке С или C++. При этом им иногда приходится писать в 5-10 раз больше кода. Итак, понятие прикрепить описатель означает примерно следующее: дать объекту класса ту функциональность, которой обладает Windows-объект, обычно описываемый структурой и адресуемый с помощью описателя (handle). Внутри многих классов MFC скрыто существуют Windows-описатели, которые должны быть правильно инициализированы. Часто, но не всегда, это делается без нашего участия. Иногда мы должны предпринять какие-то действия для инициализации описателя. В данном случае это можно сделать прямым присвоением, например m_pimgList->m_hImageList = himg; но такой способ менее надежен, так как в нем непосредственно запоминается какой-то адрес памяти. Содержимое по этому адресу система может изменить в результате наших же манипуляций с объектами, и тогда мы получим проблему под названием «Irreproducible Bug» (невоспроизводимая ошибка). Точнее будет сказать трудновоспроизводимая ошибка — самый неприятный тип ошибок, для борьбы с которыми идут в ход все средства (даже AssertValid и Dump). Значительно надежнее использовать метод Attach класса CimageList, так как в этом случае система будет следить за перемещениями структур, адресуемых описателем. При этом работает класс CHandleMap и его метод SetPermanent, которые, к сожалению, не документированы.



Связывание списка с объектом m_Tree производит функция SetlmageList, последний параметр которой (TVSIL_NORMAL) говорит о том, что тип списка обычный, то есть состоит из двух изображений. Альтернативным выбором является TVSIL_STATE, справку о нем вы получите самостоятельно, если захотите. Поместите следующий код в файл LeftView.cpp. Он вставляет в дерево новый элемент с изображением, которое ему соответствует:

void CLeftView::AddItem (HTREEITEM h, LPCTSTR s)

{

SHFILEINFO Info;

int len = sizeof(Info);

//=== Добываем изображение (маленький значок)

::SHGetFileInfo (s, 0, SInfo, len, SHGFI_ICON

| SHGFI_SMALLICON); int id = Info.ilcon;

//=== Добываем изображение в выбранном состоянии

::SHGetFileInfo (s,0,Slnfo,len,

SHGFI_ICON | SHGFI_OPENICON | SHGFI_SMALLICON);

int idSel = Info.ilcon;

//====== Копируем параметр в рабочую строку

CString sName(s);

//=== Отсекаем лишние символы (сначала в конце строки)

if (sName.Right(1) == '\\')

sName.SetAt (sName.GetLength() - 1, '\0');

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

int iPos = sNarae.ReverseFind('\\') ;

if (iPos != -1)

sName = sNarne.Mid(iPos + 1) ;

//=== Вставляем узел в дерево

HTREEITEM hNew = m_Tree.InsertltemfsName,id,idSel,h);

//====== Вставляем пустой узел

if (NotErapty(s))

m_Tree.Insertltem("", 0, 0, hNew);

}

Функция SHGetFilelnf о вызывается дважды, так как от системы надо получить два индекса изображений: для объекта файловой системы в обычном состоянии и для него же в выбранном состоянии. Метод Insertltem класса CTreeCtrl вставляет узел в дерево. Его параметры задают:

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



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

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


Обзор функции Initlnstance Внесем


FALSE;

}

AfxEnableControlContainer() ;

который представляет собой инициализацию поддержки OLE (Object Linking and Embedding), то его можно убрать, так как наше приложение не будет выполнять функции OLE-сервера или OLE-контейнера. Следующая строка:

SetRegistryKey(_T("Local AppWizard-Generated..."));

представляет собой создание нового ключа в реестре Windows для хранения некоторой информации о нашем приложении. Он действительно будет новым, если вы измените строку текста на имя вашей компании, как это было задумано при разработке функции, или на какое либо другое имя («My Soft»). После запуска приложения можно открыть реестр (в командной строке Windows дайте команду: RegEdit) и отыскать в нем по адресу HKEY_CURRENT_USER\Software вновь созданный ключ My Soft. Записывать информацию по этому ключу вы можете с помощью методов класса cwinApp, от которого происходит наш класс CTreeApp. Например, метод WriteProf ilelnt позволяет записать некое целое значение (value), соответствующее произвольной секции текущего ключа. Для эксперимента вставьте вместо строки SetRegistryKey такие три строки:

SetRegistryKey("My Soft");

WriteProfileStringC'My Data", "My Name","Alex");

WriteProfilelnt("My Data","My Age",54);

Запустите приложение, перейдите в окно реестра, обновите его (View > Refresh), найдите адрес HKEY_CURRENT_USER\Software\My Soft\Tree\My Data, поставьте в него курсор мыши и убедитесь в наличии двух записей, высвечиваемых в правом окне реестра. Удалите из реестра ключ My Soft, если вам нужен реестр, а не свалка мусора (чем он обычно и является). Уберите также учебный код из тела initinstance.

Для того чтобы увидеть, как работает функция LoadStdProf ileSettings, вызов которой виден в теле initinstance, запустите приложение и запишите хотя бы один документ (команда: File > Save). После этого вы можете найти в реестре (не забывайте освежать его) по тому же адресу новую секцию Recent File List, которая содержит запись — полный путь к только что записанному файлу. Параметр функции LoadStdProf ileSettings указывает, сколько записей может содержать список MRU (Most Recently Used) последних документов. Если вы зададите его равным нулю, то список не будет поддерживаться каркасом приложения.


Теперь можно приступить к созданию двух шаблонов документов вместо одного, рассмотренного выше. Для того чтобы задействовать второй шаблон, надо убрать из Initinstance код по созданию шаблона pDocTemplate и вставить вместо него такие строки:

//====== Создаем первый шаблон

m_pTemplTree = new CMultiDocTemplate(IDR_TreeTYPE,

RUNTIME_CLASS(CTreeDoc) ,

RUNTIME_CLASS(CTreeFrame) ,

RUNTIME_CLASS(CLeftView)) ;

//====== Помещаем его в список

AddDocTemplate(m_pTemplTree);

//====== Создаем второй шаблон

m_pTemplDraw = new CMultiDocTemplate(IDR_DrawTYPE,

RUNTIME_CLASS(CTreeDoc),

RUNTIME_CLASS(CDrawFrame),

RUNTIME_CLASS(CDrawView));

//====== Помещаем его в список

AddDocTemplate(m_pTemplDraw);

Второй шаблон тоже помещается в список шаблонов приложения. Каркас приложения устроен так, что теперь каждый раз, когда пользователь будет выбирать команду File > New, будет появляться диалог со списком шаблонов и просить его выбрать шаблон, которому должен соответствовать новый документ. Идентификатор ресурсов !DR_DrawTYPE определяется заранее, то есть в файле resource.h должна быть макроподстановка #def ine, заменяющая этот идентификатор целым положительным числом. Самым простым способом создания нового идентификатора является вызов команды Edit > Resource Symbols. Но этот способ будет некорректным

в нашем случае, так как мы поместили второй шаблон в список шаблонов, ассоциированных

с документами приложения, и его идентификатор должен быть связан с какими-то ресурсами.

Ресурсов, которые связаны со вторым шаблоном, может быть несколько, и мы покажем, как связать с ним значок, меню, панель инструментов и строковый ресурс, расположенный в таблице String Table. Последний является текстовой строкой, которая разбита символами ' \п' на отдельные части — подстроки. Каждая подстрока имеет определенное значение и используется каркасом приложения в разные моменты его жизни. Например, вторая подстрока является корнем для образования имен новых документов, и вы обычно видите ее в заголовке дочернего окна документа. Откройте окно Resource View, раскройте узел дерева под именем String Table и сделайте двойной щелчок на вложенном в него элементе. В таблице строк справа найдите iDR_TreeTYPE. Он идентифицирует комплексную строку:



\nTree\nTree\nTree Files (*.mgn)\n.mgnXnTree.Document\nTree.Document

Вы можете получить справку по всем частям этой строки, если вызовете помощь (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);

}


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


CWndGeom : public CWnd

{

public:

CTreeDoc *m_pDoc;

// Адрес документа (для удобства)

CRightview *m_pView;

// Адрес родительского окна

int m_ID;

// Индекс окна документа в массиве CRect m_Rect;

// Координаты в правом окне

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

CWndGeom (CRightview *p, int id);

~CWndGeom();

protected: DECLARE_MESSAGE_MAP()

};

В файле реализации класса измените коды конструктора, как показано ниже. Затем с помощью Studio.Net введите в класс реакции на следующие сообщения: WM_PAINT, WM_LBUTTONDOWN и WM_MOUSEMOVE. Цель этих действий такова. При наведении курсора мыши на одно из окон, управляемых классом CWndGeom, оно должно проявить признаки готовности быть выбранным. Для этого рисуем в нем обрамляющий прямоугольник, который исчезает при выходе указателя мыши за пределы окна. Эта функциональность реализуется за счет пары функций

SetCapture - ReleaseCapture. Метод CWnd: : SetCapture захватывает текущее окно как адресат для последующих сообщений мыши независимо от позиции курсора. Поэтому при перемещении курсора мыши можно выйти из пределов клиентской области окна и все равно получить и обработать сообщение им_ MOUSEMOVE. На этом свойстве окна и построен алгоритм его подсветки. Функция ReleaseCapture «освобождает мышь», то есть вновь восстанавливает обычный порядок обработки мышиных сообщений. Мы вызываем функцию после того, как обнаружен факт выхода за пределы окна и снята подсветка, то есть стерт обрамляющий прямоугольник:

CWndGeom::CWndGeom(CRightView *p, int id)

{

//====== Запоминаем адрес родительского окна

m_pView = р;

//====== Запоминаем адрес документа

m_pDoc = p->GetDocument();

//====== и индекс окна в массиве

m_ID = id;

}

void CWndGeom::OnPaint()

{

CPaintDC dc(this);

dc.SetMapMode(MM_ISOTROPIC) ;

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

dc.SetWindowOrg (m_pDoc->m_szDoc.cx/2, m_pDoc->m_szDoc.cy/2), dc.SetWindowExt(m_pDoc->m_szDoc);

//====== Узнаем текущие размеры окна

GetClientRect(&m_Rect);


int w = m_Rect.Width (), h = m_Rect.Height ();

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

dc.SetViewportOrg (w/2, h/2);

dc.SetViewportExt (w, -h);

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

//=== его изобразить себя в подготовленном контексте m_pDoc->m_Shapes[m_ID].Draw(Sdc);

}

void CWndGeom: :OnLButtonDown (UINT nFlags, CPoint point)

{

//====== Изменяем дежурный полигон

m_pDoc->m_Poly = m_pDoc->m_Shapes [m_ID] ;

//== Если не было CDrawView, то создаем его

if (m_pDoc->MakeView() )

return;

//=== Если он был, то находим его и делаем активным

CView *pView = m_pDoc->GetView (RUNTIME_CLASS (CDrawView) ;

{

(CMDIChildWnd*)pView->GetParentFrame ()

} ->MDIActivate () ;

//====== Перерисовка с учетом изменений

pView->Invalidate ( ) ;

Все «мышиные» сообщения сопровождаются параметрами, которые информируют нас о том, где и как произошло событие. Первый параметр есть набор битов, указывающих, какие виртуальные клавиши были нажаты в момент события. Например, если nFlags содержит бит с именем MK_CONTROL (символическая константа), то это означает, что в момент нажатия левой кнопки мыши также была нажата клавиша Ctrl. Второй параметр содержит координаты (х, у) местоположения курсора в момент события. Они заданы относительно верхнего левого угла клиентской области окна:

void CWndGeom: lOnMouseMove (UINT nFlags, CPoint point)

{

//====== Если указатель мыши в пределах окна,

if (m_Rect.Pt!nRect (point))

{

//====== то захватываем мышь, выбираем перо

//====== и рисуем обрамляющий прямоугольник

SetCapture () ;

CClientDC dc(this) ;

CPen pen (PS_SOLID, 4, RGB (192, 192, 255));

dc.SelectObject (&pen);

dc.MoveTo(m_Rect.left+4, m_Rect . top+4) ;

dc.LineTo (m_Rect.right-4, m_Rect . top+4) ;

dc.LineTo (m_Rect.right-4, m_Rect .bottom-4) ;

dc.LineTo (m_Rect.left+4, m_Rect .bottom-4) ;

dc.LineTo (m_Rect.left+4 , m_Rect . top+4) ;

}

else

{

ReleaseCapture () ;

// Освобождаем мышь Invalidated;

// Прямоугольник будет стерт

}

}

Так как в коде функции OnLButtonDown содержится обращение к объекту класса CDrawView, то необходимо в список директив препроцессора текущего файла (RightView.cpp) вставить еще одну: #include "DrawView.h".


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


CDrawView::OnUpdateEditNewpoly(CCradUI *pCmdUI)

{

pCmdUI->SetCheck(m_bNewPoints);

}

Метод SetCheck вспомогательного класса ccmdui устанавливает флажок рядом с командой меню, если параметр имеет значение TRUE, или снимает его, если параметр имеет значение FALSE. Состояние кнопки на инструментальной панели синхронизировано с состоянием команды меню, имеющей тот же идентификатор.

Следующим шагом в развитии приложения будет введение в действие второй панели инструментов IDR_Draw_TYPE. Загрузка из ресурсов панели инструментов осуществляется методом LoadToolBar класса CToolBar. Так как объект этого класса (m_wndToolBar) хранится в классе главного окна (CMainFrame), то и смену панелей инструментов целесообразно выполнять в этом же классе. Введите в него новый метод:

void CMainFrame::ChangeToolbar(UINT tb)

{

//=== в параметре tb будет передан идентификатор панели

m_wndToolBar.LoadToolBar (tb) ;

//=== Перерисовка toolbar

RecalcLayout();

}

Метод CFrameWnd::RecalcLayout занимается перерисовкой панели инструментов и пересчетом размеров клиентской области окна, так как панель инструментов хоть и управляется классом главного окна, но расположена в клиентской области окна, отнимая у нее часть полезной площади.

Имея в классе главного окна такую функцию, как ChangeToolbar, мы просто должны вызывать ее в нужные моменты, подавая на вход идентификатор той или иной панели. Осталось правильно определить те моменты, когда надо производить смену панелей. Очевидно, это моменты перевода фокуса из окна типа CTreeFrame в окно типа CDrawFrame и наоборот.

Здесь важно понять, что фокус на самом деле попадает в одно из дочерних окон 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<CLeftview*>

(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<CString> 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


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

Открыть окно Class View, установить фокус на имя класса CLeftView и перейти в окно Properties.

В окне Properties нажать кнопку с подсказкой Messages, а затем кнопку Categorized.

Нажать маркер (-) Common в верхнем левом углу прокручиваемого списка сообщений так, чтобы он изменился на (+). Тем самым вы скрываете часть списка с перечнем всех обычных сообщений Windows.

В оставшейся части списка найти сообщение =TVN_ITEMEXPANDING и в выпадающем списке справа выбрать действие <Add>.

Повторить действия пункта 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.


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


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<cstring>, который чуть позже мы вставим в число членов класса документа. А сейчас дополните класс 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());

}

}



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


CToolBar m_wndToolBar;

CStatusBar m_wndStatusBar;



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


CPolyDlg::DoDataExchange(CDataExchange* pDX)

{

//====== Связывание Control-переменных с ползунками

DDX_Control(pDX, IDC_BSLIDER, m_bSlider);

DDX_Control(pDX, IDCJ3SLIDER, m_gSlider);

DDX_Control(pDX, IDC_RSLIDER, m_rSlider);

//==== Связывание Control-переменных с нестандартными

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

DDX_Control(pDX, IDC_COLOR, m_cColor) ;

DDX_Control(pDX, IDC_BLUE, m_cBlue);

DDX_Control(pDX, IDC_GREEN, m_cGreen);

DDX_Control (pDX, IDC_RED, m_cRed) ;

//==== Связывание Value-переменных с нестандартными

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

DDX_Text(pDX, IDC_BLUE, m_nBlue);

DDV_MinMaxUInt(pDX, m_nBlue, 0, 255);

DDX_Text (pDX, IDC_GREEN, m_nGreen);

DDV_MinMaxUInt(pDX, m_nGreen, 0, 255);

DDX_Text(pDX, IDC_RED, m_nRed) ;

DDV_MinMaxUInt(pDX, m_nRed, 0, 255);

DDX_Text(pDX, IDC_PEN, m_nPen);

DDV_MinMaxUInt(pDX, m_nPen, 1, 100);

//==== Вызов родительской версии функции обмена CDialog::DoDataExchange(pDX);

}



Список изображений, ассоциируемый с деревом


Дерево выглядит значительно лучше, если с каждой его ветвью связать растровое изображение (bitmap image). Обычно с деревом ассоциируется список изображений, управляемый объектом класса cimageList. В общем случае с каждым узлом дерева можно связать два изображения. Одно — для узла в нормальном состоянии, другое — в выбранном. Мы уже ввели в состав класса переменную m_plmgList типа cimageList*, которая должна указывать на сформированный список. Немного позже мы попросим систему дать нам Windows-описатель (HIMAGELIST) поддерживаемого ею списка изображений для дисков, папок и файлов. Однако программист должен уметь самостоятельно формировать список произвольных растровых изображений и связывать его с объектом класса CTreeCtrl. Покажем, как это делается. Создайте несколько bitmap-изображений и присвойте им идентификаторы IDB_IDB_2 и т. д. Последнему изображению присвойте имя IDB_N. Для этого:

Установите фокус на узел Тгее.гс в окне 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 и при желании возвратите язык, заменив на тот, который принят по умолчанию.


Вернемся К функции OnlnitialUpdate. После строки m_Tree.SetlmageList... вставьте фрагмент, который задает форму дерева из трех узлов (или ветвей):



//====== Вставляем узел верхнего уровня иерархии

gtv.hParent = TVI_ROOT;

//====== Вставляем в конец списка

gtv.hlnsertAfter = TVI_LAST;

//====== Формат узла — два изображения и текст

gtv.item.mask = TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIFJTEXT;

//=== Индекс изображения для узла в обычном состоянии

gtv.item.iImage = 0;

//=== Индекс изображения для узла в выбранном состоянии

gtv.item.iSelectedlmage = 1;

//====== Текст, именующий узел

gtv.item.pszText = "First";

11====== Описатели трех ветвей

HTREEITEM hi, h2, h3;

//====== Вставка первого узла

hi = m_Tree.Insertltem(Sgtv);

//====== Первый узел будет родителем второго

gtv.hParent = h1;

//====== Атрибуты второго узла

gtv.item.iImage = 1;

gtv.item.pszText = "Second";

//====== Вставка второго узла

h2 = m_Tree.Insertltem(Sgtv);

//====== Второй, узел будет родителем третьего

gtv.hParent = h2;

gtv.item.ilmage = 2;

gtv.item.pszText = "Third";

//====== Вставка третьего узла

h3 = m_Tree.Insertltem(Sgtv);

Запустите приложение, и если вы не забыли создать bitmap-изображения, то они должны появиться слева от текстового ярлыка узла (рис. 5.1). Проанализируйте вложенность узлов дерева. Теперь замените в строке gtv.hParent = b2; b2 на b1и проверьте результат. Затем рекомендуем заменить b1 на константу TVI_ROOT и вновь посмотреть, что получится. Обратите внимание на то, что изображения изменяются при выборе узлов, то есть при переводе курсора мыши с одного узла на другой.



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


Тестирование Приложения, даже


CWndGeom:

void CWndGeom::OnMouseMove(UINT nFlags, CPoint point)

{

//====== Два прямоугольника (CWndGeom и CRightView)

CRect rChild, rParent;

//=== Определяем экранные координаты (не клиентские!)

GetWindowRect(rChild) ;

m_pView->GetWindowRect (rParent) ;

//=== Если есть полосы прокрутки, то уменьшаем

//=== прямоугольник окна на толщину полос

if (m_pView->m_szScroll.cx - m_j>View->m_szView.cx > 0)

rParent . right -= SM_CXHSCROLL;

if (m_pView->m_szScroll.cy - m_pView->m_szView.cy > 0)

rParent. bottom -= SM_CYVSCROLL ;

//=== Ищем пересечение прямоугольников, обрезая rChild

rChild.IntersectRect (rChild, rParent);

//=== Приводим к экранным координаты указателя мыши

ClientToScreen (Spoint) ;

//=== Если мышь попала в усеченный прямоугольник,

if ( rChild. PtlnRect (point))

{

//=== то демонстрируем активное состояние,

// изображая рамку внутри прямоугольника CWndGeom

if (GetCaptureO != this)

{

SetCapture() ;

//=== Координаты относительные (клиентские)

CRect r (mJRect) ;

r.DeflateRect (4, 4);

CClientDC do (this) ;

//====== Обрамляем выбранный рисунок

dc.FrameRect (Sr, SCBrush (RGB (192, 192, 255) ) ) ;

}

else

{

//=== Это происходит один раз при выходе из окна

ReleaseCapture () ;

Invalidate () ;

}

}

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

if (rChild. PtlnRect (point) )

{

if (GetCaptureO != this)

{

SetCapture () ;

CPen pen (PS_SOLID, 4, RGB (192, 192, 255) );

CClientDC dc(this) ;

dc. SelectObject (&pen) ;

CRect r (m_Rect) ;

//====== Уменьшаем прямоугольник

r .DeflateRect (4,4) ;

//=== Выбираем прозрачную кисть для того, чтобы

//=== не закрасить его содержимое

dc. SelectObject (GetStockObject (NULL_BRUSH) ) ;

dc. Rectangle (r) ;

}

}

На рис. 5.4 приведен вид приложения в момент, когда курсор мыши расположен над окном, отображающим данные неактивного документа. Рамка и курсор обозначают состояние готовности к произведению выбора.

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



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


Настройка стартового кода

Список изображений, ассоциируемый с деревом

Обращаемся к операционной системе

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


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


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<m_Poly.m_Points.size(); 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 (первую из двух выпавших строк).

Создайте обработчик, выбрав <Add> в выпадающем списке справа от COMMAND.



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

В теле обработчика следует установить флаги состояния, уничтожить все вершины дежурного полигона и перерисовать представление:

void CDrawView::OnEditNewpoly(void)

{

//====== Включаем/Выключаем режим ввода вершин

m_bNewPoints = !m_bNewPoints;

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

m_bReady = false;

m_bLock = false;

//====== Если режим включен, то уничтожаем вершины

if (m_bNewPoints)

{

GetDocument()->m_Poly.m_Points.clear() ;

Invalidate();

}

}

Запустите приложение, выберите шаблон Draw и дайте команду Edit > New Poly. Щелкайте левой кнопкой мыши разные места клиентской области окна и наблюдайте за трансформациями полигона m_Poly при добавлении в контейнер его точек новых значений. Мысленно проследите за преобразованиями координат, которые происходят в эти моменты. Вы помните, что мышь дает аппаратные координаты, а в контейнер попадают World-координаты вершин полигона?


Вспомогательные функции Задание


CPolygon::MakeStar()

{

m_Points.clear();

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

double pi = 4. * atan(l.), // Углы

al = pi / 10.,

а2 = 3. * al,

//====== 2 характерные точки

xl = cos (al),

yl = sin(al),

x2 = cos(a2),

y2 = sin(a2);

//=== Вещественные (World) координаты углов звезды m_Points.push_back(CDPoint(0., 1.));

m_Points.push_back(CDPoint <-x2, -y2));

m_Points.push_back(CDPoint( xl, yl) ) ;

m_Points.push_back(CDPoint(-xl, yl)) ;

m_Points.push_back(CDPoint( x2, -y2));

//====== Габариты звезды

m_ptLT = CDPoint(-xl, 1.);

m_ptRB = CDPoint( xl,-y2);

//====== Генерация треугольника

void CPolygon::MakeTria() {

m_Points.clear();

double pi = 4. * atand(1.);

a = pi / 6.;

x = cos (a) ;

у = sin(a);

m_Points.push_back (CDPoint(0., 1.));

m_Points,push_back (CDPoint(-x, -y) );

m_Points.push_back (CDPoint( x, -y));

m_ptLT = CDPoint (-x, 1.) ;

m_ptRB = CDPoint ( x,-y);

//====== Генерация пятиугольника

void CPolygon::MakePent()

{

m_Points.clear ();

double pi = 4. * atan(l.),

al = pi / 10.,

a2 - 3. * al,

xl = cos(al),

yl = sin(al),

x2 = cos(a2),

y2 = sin(a2);

// Вещественные (World) координаты углов пятиугольника m_Points.push_back(CDPoint (0 ., 1.));

m_Points.push_back(CDPoint(-xl, yl));

m_Points.push_back(CDPoint(-x2, -y2));

m_Points.push_back(CDPoint( x2, -y2));

m_Points.push_back(CDPoint( xl, yl));

m_ptLT = CDPoint(-xl, 1.);

m_ptRB = CDPoint( xl,-y2);



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


"RightView.h"

#include "DrawView.h"

Затем перейдем к реализации заявленного в классе документа метода Getview (поиск адреса нужного представления). Его параметром служит адрес статической структуры типа CRuntimeClass, которая присутствует во всех классах, произведенных от cob j ect. Она является общей для всех объектов одного и того же класса и содержит ряд полезных полей, в том числе и поле m_lpszClassName, которое позволяет узнать имя класса на этапе выполнения программы. Обычно для того, чтобы узнать, принадлежит ли объект (адрес структуры CRuntimeClass которого вы знаете) тому или иному классу, пользуются функцией isKindOf, унаследованной от CObject. Она, в свою очередь, для ответа на этот вопрос использует поле m_lpszClassName структуры CRuntimeClass:

CView* CTreeDoc::GetView(const CRuntimeClass* pClass)

{

// Становимся в начало списка представлений документ^

POSITION pos = GetFirstViewPosition();

//====== Пессимистический прогноз

CView *pView = 0;

//====== Цикл поиска нужного представления

while (pos)

{

pView = GetNextView(pos);

//=== Если нашли, то возвращаем адрес

if (pView->IsKindOf(pClass))

break;

}

//===== Возвращаем результат поиска return pView;

}

В процессе работы с MDI-приложением пользователь закрывает одни документы и открывает другие. Вновь открытый документ в начальный момент представлен одним из двух возможных типов окон: либо расщепленным окном типа CTreeFrame, которое содержит два окна CLef tview и CRightview, либо обычным MDI-child-окном типа CDrawFrame, которое содержит одно окно CDrawView. В ситуации, когда пользователь по картинке выбрал в правом окне один из документов, по сценарию необходимо создать новое окно типа CDrawFrame и в его клиентскую область поместить альтернативное представление (CDrawView) выбранного документа. Целесообразно реализовать и обратный сценарий, когда, имея окно типа CDrawView, пользователь хочет создать окно типа CTreeFrame, обрамляющего другие два представления документа.


Создание и инициализация новых окон того или иного типа в MDI-приложени-ях производится с помощью методов класса CDocTemplate, так как именно шаблон документа хранит информацию обо всех членах квартета, ответственных за создание окна документа. Список всех шаблонов документов, поддерживаемых приложением, хранит объект theApp класса СТгееАрр. Класс cwinApp, от которого происходит класс СТгееАрр, предоставляет стандартные методы для работы со списком шаблонов. Метод GetFirstDocTemplatePosition устанавливает позицию (переменную вспомогательного типа POSITION для работы со списками) на первый шаблон списка. Метод GetNextDocTemplate обычным образом возвращает адрес текущего шаблона и после этого сдвигает позицию на следующий элемент списка. Подобный стиль работы со списками поддерживается и другими классами MFC. Привыкнув к нему, вы сэкономите массу усилий в будущем.

Однако в нашем случае, когда существуют только два шаблона документов, нет необходимости искать в списке шаблонов. Мы просто запомнили их адреса (m_pTemplTree, m_pTemplDraw) в объекте theApp класса СТгееАрр. Теперь в любой момент жизни приложения мы можем добыть их и использовать, например для создания новых окон того или иного типа. Ниже приведен метод MakeView класса CTreeDoc, который выполняет указанное действие.

Каркас 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<CTreeApp*> (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<CRightView*>

(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<CRightView*>

(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<CTreeDoc*> (GetActiveDocument());

pDoc->m_bTreeExist = false;

CMDIChildWnd::OnClose();

}

void CDrawFrame::OnClose()

void CDrawFrame::OnClose()

{

CTreeDoc *pDoc = dynamic_cast<CTreeDoc*> (GetActiveDocument());

pDoc->m_bDrawExist = false;

CMDIChildWnd::OnClose() ;

}

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

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