В соответствии с архитектурой «документ — представление» мы должны ввести в класс документа некоторые новые структуры данных для хранения информации о файлах документов, обнаруженных в выбранной пайке или логическом диске. Файловые пути хранятся в контейнере текстовых строк типа 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, которая играет роль обратного указателя. Такой прием, когда вложенный объект помнит адрес объемлющей его структуры данных, очень распространен в объектно-ориентированном программировании. Он существенно упрощает обмен данными между двумя объектами.
Примечание 1
Примечание 1
Здесь трудно обойтись без специального метода установки обратного указа-теля, в нашем случае метода Set. Дело в том, что при создании документа надо сначала создать вложенные в него объекты других классов (вспомните правило: «C++ уважает гостей»). Но в этот момент им нельзя передать адрес документа, так как он еще не создан. В таких случаях поступают следующим образом. В заголовке конструктора документа создают пустые объекты (вызывают default-конструкторы вложенных объектов), а затем в теле конструктора документа, когда он уже существует, для вложенных объектов вызывают метод, устанавливающий обратный указатель. При этом объекту передают указатель на документ (на объект собственного класса). Например: m_Poly.Set(this);
Обилие методов класса CPolygon сделано «на вырост». Сейчас каждый документ для простоты представлен одним полигоном. Реальные конструкции можно задать в виде множества полигонов. При этом каждый из них должен знать свои габариты. Метод GetRect позволяет вычислять и корректировать габариты полигона. Если вы будете применять эти идеи в более сложном проекте, то вам понадобится множество других методов. Например, методы, определяющие факт самопересечения полигона или взаимного их пересечения.
Главными методами, которые реализуют концепцию архитектуры «документ — представление», являются Serialize и Draw. Метод Serialize позволяет общаться с файлами. Его особенность состоит в том, что он позволяет как записывать все данные объекта в файл, точнее в архив, так и читать их из файла. Здесь опять проявятся преимущества наследования от cobject, так как объекты классов, имеющих такого авторитетного родителя, обычно сами умеют себя сериализовывать.
Примечание 2
Примечание 2
Термин «сериализация» приходится брать на вооружение, так как он довольно емкий, и чтобы его заменить, надо произнести довольно много слов о последовательном (in series) помещении данных объекта в архив, который связан с файлом. Кроме того, надо сказать о том, что в классе CArchive переопределены операции « и ». Просмотрите почти пустое тело функции Serialize в классе документа. Оно, тем не менее, намекает нам, как разделяются две разновидности общения с архивом. Вызов функции CArchive::IsStoring() возвращает ненулевое значение в случае, если архив используется для записи данных.
Новый класс CPolygon должен иметь родителя CObject, с тем чтобы он мог воспользоваться его мощным оружием — сериализацией. При этом в объявлении класса должен присутствовать макрос:
DECLARE_SERIAL(CPolygon)
который влечет свое продолжение — другой макрос
IMPLEMENT_SERIAL(CPolygon, CObject, 1)
Последний должен быть расположен в файле реализации класса. Третий параметр (wSchema) этой макроподстановки задает номер версии приложения. Номер схемы кодируется и помещается в архив вместе с другими сохраняемыми данными. Это позволяет корректно обойтись в такой ситуации.
Предположим, что имеются файлы с расширением mgn, в которых хранятся данные о магнитах, созданных нашим приложением. Затем допустим, что мы внесли изменения в коды приложения и добавили в класс CPolygon еще одно какое-то поле данных. Теперь, записывая данные в архив (файл), также получим файл с расширением mgn, но другого формата. После этого мы не сможем правильно читать старые файлы. Если не предпринять никаких мер, то данные будут прочитаны неверно, а это часто приводит к непредсказуемому поведению программы. Механизм версий справляется с этой проблемой, но вы не должны забывать вовремя менять номер версии. При каждом изменении в структуре сохраняемых данных следует изменять номер версии. При попытке прочитать файл, соответствующий другой версии, каркас приложения просто выдаст сообщение о несовпадении версий и закроет файл данных.
С учетом сказанного рассмотрим, как должна выглядеть реализация нового класса. Следующие функции и макросы необходимо поместить в начало файла TreeDoc.cpp, после директив препроцессора:
IMPLEMENT_SERIAL(CPolygon, CObject, 1)
//====== Конструктор по умолчанию
CPolygon::CPolygon()
{
m_pDoc = 0; // Пока не знаем обратного адреса
MakeStar(); // Зададим полигон в виде звезды
}