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

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

#include "RightView.h"

#include "DrawView.h"

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

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

{

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

POSITION pos = GetFirstViewPosition();

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

CView *pView = 0;

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

while (pos)

{

pView = GetNextView(pos);

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

if (pView->IsKindOf(pClass))

break;

}

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

return pView;

}

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

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

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

Примечание

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

Вы помните, что в любой точке программы мы имеем право вызвать глобальную функцию MFC. Напомним, однако, что почти все глобальные объекты MFC имеют префикс Afx (Application frameworks) — каркас приложения. Среди них есть много действительно полезных функций. Посмотрите справку по индексу Af х, и вы увидите все множество. Традиционно, для того чтобы достать адрес объекта theApp класса приложения, пользуются функцией Af xGetApp. Существует и второй способ — непосредственно использовать глобально определенный объект theApp, но для этого необходимо в начало срр-файла, где предполагается его использовать, поместить строку, разрешающую проблему видимости объекта theApp:

extern СТгееАрр theApp; // Определен в другом месте

В файл реализации класса CTreeDoc вставьте тело функции MakeView, которое приведено ниже. В ней реализован доступ к приложению с помощью глобальной функции AfxGetApp, но вы можете опробовать и второй способ, заменив "рАрр->" на " theApp. " и учтя сказанное выше. При этом также отпадает необходимость в строке кода СТгееАрр* рАрр = (СТгееАрр*) Af xGetApp ();.

bool CTreeDoc::MakeView()

{

//==== Если недостает какого-либо из представлений

if (!m_bDrawExist || !m_bTreeExist)

{

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

CTreeApp* pApp = (CTreeApp*) AfxGetApp ();

CDocTemplate *pTempl;

// ====== Выбираем шаблон недостающего типа

if ( !m_bDrawExist)

{

pTempl = pApp->m_pTemplDraw;

m_bDrawExist = true;

}

else

{

pTempl = pApp->m_pTemplTree;

m_bTreeExist = true;

// Создаем окно документа

// Тип рамки и представления определяется шаблоном

CFrameWnd *pFrarae = pTempl->CreateNewFrame (this, 0) ; pTempl->InitialUpdateFrame (pFrarae, this ) ;

return true;

}

return false;

}

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

CTreeApp* pApp = dynamic_cast<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 поступает при инициализации левого окна.