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


В этом уроке мы подробно рассмотрим процесс разработки MDI-приложения, в котором один тип документов взаимодействует с несколькими своими представлениями. В рамках архитектуры «документ — представление» принято использовать следующие термины:

  • документ — обозначает класс, производный от MFC-класса CDocument и вобравший в себя (инкапсулирующий) функциональность данных документа;
  • представление — обозначает класс, производный от MFC-класса cview и инкапсулирующий функциональность окна, дочернего по отношению к окну-рамке. В нем в том или ином виде представлены данные документа.

Главным моментом в архитектуре является то, что один документ может иметь несколько связанных с ним представлений, но каждое из них может быть связано лишь с одним документом.
Особенностью разрабатываемого приложения является то, что в одном из представлений, управляемых классом cscrollview, пользователь сможет просматривать в качестве «картинок» — чертежей или схем, выбирать и открывать документы своего приложения, которые расположены в файлах с различными адресами. Навигацию по файловому дереву будем осуществлять с помощью второго представления, которым управляет класс CTreeView. Классы CScrollView и CTreeView являются специализированными потомками класса cview. Класс CTreeView тесно связан с классом CTreeCtrl, который разработан как элемент управления произвольным деревом. Мы должны научиться им управлять.
Документ, выбранный пользователем с помощью двух предыдущих представлений, отображается в третьем, производном от cview, которое служит посредником между пользователем и данными документа. В его окне пользователь сможет редактировать данные документа. В качестве данных мы используем динамический массив (контейнер) точек с вещественными координатами, который удачно моделирует произвольный чертеж — двухмерную проекцию какого-либо элемента конструкции. Идеи, заложенные в этом учебном приложении, использованы в реальном проекте по расчету физических полей, описываемых дифференциальными уравнениями в частных производных. В частности, производились расчеты поля магнитов, отсюда проистекает выбранное нами расширение (mgn) для документов приложения. В задачах такого рода исходными являются данные о геометрии расчетной области. Именно она наиболее точно определяет документ (вариант расчета). Если число таких геометрий велико, то поиск варианта по картинке расчетной области существенно упрощает жизнь исследователя физических полей. В связи с этим был получен заказ — ввести в проект возможность поиска и выбора документа по миниатюрному графическому представлению (схеме) геометрии расчетной области. Упрощенная реализация этой части проекта рассмотрена ниже. Начнем с создания стартовой заготовки MDI-приложения.

  1. На странице VS Home Page выберите команду (гиперссылку) Create New Project.
  2. В окне диалога New Project выберите уже знакомый вам тип проекта: MFC Application, задайте имя проекта Tree и нажмите ОК.
  3. В окне мастера MFC Application Wizard выберите вкладку Application Type и сделайте следующие установки: Multiple documents. Windows Explorer, Document/View procedure support, use MFC in a shared DLL
  4. Перейдлте на другую страницу мастера (Document Template Strings) и в поле File extension: задайте расширение mgn для файлов документов будущего приложения.
  5. На странице User Interface Features поставьте флажок Child maximized, для того чтобы окна документов занимали всю клиентскую область главного окна приложения. Там же установите флажок Maximized, для того чтобы само главное окно занимало весь экран.
  6. Так как мы собираемся вводить в проект новые классы для управления окнами, различным образом представляющими документ, целесообразно изменить предлагаемые мастером имена классов. На странице Generated Classes измените имена: CTreeView на CRightView, CChildFrame на CTreeFrame. Будет удобнее ориентироваться в файлах проекта, если изменить также и имена файлов ChildFrm на TreeFrm, TreeView на RightView (для h- и срр-файлов).
  7. Для класса CRightView произведите замену родителя (в поле Base Class). Вместо CListView выберите класс CScrollView.
  8. Нажмите кнопку Finish.

  
Настройка стартового кода
Просмотрите плоды работы мастера в окне 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 это делается так:

  1. Поставьте фокус в узел Tree окна Class View или окна Solution Explorer и дайте команду View > Property Pages (Alt+Enter).
  2. В появившемся диалоге Property Pages раскройте узел дерева C/C++ и выберите элемент Language.
  3. В таблице окна справа найдите свойство 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. Вы можете программно изменять этот порядок.

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


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

  1. Установите фокус на узел Тгее.гс в окне Resource View и вызовите контекстное меню.
  2. Выберите команду Add Resource, в окне появившегося диалога выберите элемент Bitmap и нажмите кнопку New.
  3. Перейдите в окно Properties. Для удобства вытащите его из блока окон (команда Floating) и отбуксируйте в сторону. В окне задайте идентификатор ID = IDB_1.
  4. Установите фокус на пустом поле будущего изображения. При этом содержимое окна Properties изменится, позволив вам задать размеры (Height = 16, Width = 16).
  5. Средствами редактора создайте изображение. Для создания второго изображения можно воспользоваться копией первого. В окне Resource View установите фокус на узел дерева ресурсов, соответствующий первому изображению, и вызовите контекстное меню.
  6. В этом меню выберите команду Insert Copy, затем в появившемся окне диалога измените язык на любой, отличный от текущего (он выведен в окне Language).
  7. Выполните двойной щелчок на новом узле дерева ресурсов в окне Resource View, измените изображение, вновь переведите фокус на узел дерева и перейдите в окно Properties.
  8. Измените IDB_1 на IDB_2 и при желании возвратите язык, заменив на тот, который принят по умолчанию.

Повторив эту процедуру столько раз, сколько необходимо иметь различающихся изображений, закончите тем, что последнему из них присвойте ID = IDB_N. Имена идентификаторов произвольны. Важно только то, что их числовые эквиваленты должны следовать подряд. Если вы не отрывались на создание других ресурсов, то Studio.Net сделала это автоматически. Будем считать, создано 3 изображения, и индекс последнего из них равен IDB_3. Для того что бы связать список с деревом вместо строки m_Tree. Insert I tern ("Item", 0,0); В функцию OnlnitialUpdate вставьте такой фрагмент:
//====== Традиционный для MS двухступенчатый способ
//====== создания нового объекта - списка изображений
m_pImgList = new CimageList;
m_pImgList->Create(16, 16, ILC_MASK, 0, 32);
for (UINT nID = IDB_1; nID <= IDB_3; nID++)
{
//====== Временный объект
CBitmap bitmap;
//====== Загрузка из ресурсов
bitmap.LoadBitmap(nID);
//====== Добавление в конец списка изображений
m_pImgList->Add(Sbitmap, (COLORREF)OxFFFFFF);
//====== Освобождаем память

bitmap.DeleteObject();
}
//=== Связывание списка изображений с объектом
CTreeCtrl m_Tree.SetlmageList(m_pImgList, TVSIL_NORMAL);
Параметры функции Create задают размеры изображений, их тип, начальный размер списка и квант его приращения при вставке новых изображений. Цикл загрузки изображений и вставки их в список будет корректно работать, только если их индексы следуют подряд. Метод SetlmageList связывает список с деревом, то есть элементом управления m_Tree типа CTreeCtrl. После этого можно начать формировать дерево.
Вставку новых ветвей осуществляют несколькими способами. Рассмотрим один из них, использующий специальную структуру типа TVINSERTSTRUCT. Просмотрите справку по этому типу, чтобы знать состав полей структуры. Обычно необходима одна глобальная структура такого типа. Это удобно, так как ею могут пользоваться несколько разных функций. В начало файла LeftView.cpp (после директив препроцессора) вставьте определение:
TVINSERTSTRUCT gtv; // Глобальная структура
Вернемся К функции OnlnitialUpdate. После строки m_Tree.SetlmageList... вставьте фрагмент, который задает форму дерева из трех узлов (или ветвей):
//====== Вставляем узел верхнего уровня иерархии
gtv.hParent = TVI_ROOT;
//====== Вставляем в конец списка
gtv.hlnsertAfter = TVI_LAST;
//====== Формат узла — два изображения и текст
gtv.item.mask = TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIFJTEXT;
//=== Индекс изображения для узла в обычном состоянии
gtv.item.iImage = 0;
//=== Индекс изображения для узла в выбранном состоянии
gtv.item.iSelectedlmage = 1;
//====== Текст, именующий узел
gtv.item.pszText = "First";
11====== Описатели трех ветвей
HTREEITEM hi, h2, h3;
//====== Вставка первого узла
hi = m_Tree.Insertltem(Sgtv);
//====== Первый узел будет родителем второго
gtv.hParent = h1;
//====== Атрибуты второго узла
gtv.item.iImage = 1;
gtv.item.pszText = "Second";
//====== Вставка второго узла
h2 = m_Tree.Insertltem(Sgtv);
//====== Второй, узел будет родителем третьего
gtv.hParent = h2;
gtv.item.ilmage = 2;
gtv.item.pszText = "Third";
//====== Вставка третьего узла
h3 = m_Tree.Insertltem(Sgtv);
Запустите приложение, и если вы не забыли создать bitmap-изображения, то они должны появиться слева от текстового ярлыка узла (рис. 5.1). Проанализируйте вложенность узлов дерева. Теперь замените в строке gtv.hParent = b2; b2 на b1и проверьте результат. Затем рекомендуем заменить b1 на константу TVI_ROOT и вновь посмотреть, что получится. Обратите внимание на то, что изображения изменяются при выборе узлов, то есть при переводе курсора мыши с одного узла на другой.

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


Теперь, когда вы научились управлять формой дерева, мы продолжим развитие приложения. Используя клавишу 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). Вы помните, что в «непустые» узлы мы вставляли нулевые элементы. Рекомендуем с
учебными целями ввести исправления и добиться демонстрации не только папок, но и файлов. Убедитесь в том, что различным типам файлов соответствуют разные изображения. Они, как вы помните из третьего урока и знаете из опыта общения с Windows, определены на этапе регистрации значка приложения или его документа.
  
Реакция на уведомляющие сообщения CTreeCtrl
Когда пользователь раскрывает узел дерева, то встроенный в класс CTreeView объект класса CTreeCtrl посылает родительскому окну (нашему представлению CLef tview) уведомляющее сообщение. Оно работает по схеме WM_NOTIFY, которую мы уже рассматривали. Наш класс CLef tview должен реагировать на это сообщение, сканировать раскрываемую папку или логический диск и изменять дерево, вставляя в раскрываемую ветвь новые объекты файловой системы, которые обнаружены внутри папки или диска. Для того чтобы ввести в класс способность реагировать на рассматриваемое сообщение, вы должны:

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

Буква N в имени сообщения говорит о том, что сообщение является уведомляющим, а знак равенства перед ним означает, что оно принадлежит к особой группе отражаемых (reflected) сообщений. В версиях MFC (до 4.0), не было обработчиков отражаемых сообщений. Теперь к старому механизму обработки уведомляющих сообщений от дочерних {child) элементов добавился новый, который позволяет произвести обработку уведомляющего сообщения в классе самого элемента. Уведомляющее сообщение как бы отражается {reflects) назад в класс дочернего окна (элемента управления CTreeCtrl). Этот сценарий мог бы быть реализован и в классе, производном от CTreeCtrl, но нам нет смысла создавать такой класс, так как возможности класса CLef tview вполне достаточны для обработки обоих сообщений. Здесь важен лишь тот факт, что можно перехватить управление в те моменты, когда пользователь манипулирует с деревом.
Первое сообщение (=TVN_ITEMEXPANDING) поступает в момент нажатия маркера (+). Дерево в этот момент еще не раскрылось. Здесь мы должны притормозить процесс перерисовки дерева до того момента, пока не получена вся информация о содержимом раскрываемого узла.
Саму информацию мы будем добывать в теле функции, обрабатывающей второе сообщение (=TVN_ITEMEXPANDED). Оно приходит после того, как узел дерева раскрылся (но не обязательно перерисовался). Здесь мы должны реализовать два варианта развития событий: узел открывается впервые и узел открывается повторно.
Третье сообщение (=TVN_SELCHANGED) приходит в момент, когда пользователь нажал кнопку в пределах самого узла, то есть он выбрал (select) узел. Начнем с обработки первого сообщения. Измените тело функции Onltemexpanding так, чтобы оно имело вид:
void CLeftView::0nltemexpanding (NMHDR* pNMHDR, LRESULT* pResult)
{
//====== Преобразование типа указателя
NM_TREEVIEW* p = (NM_TREEVIEW*)pNMHDR;
//====== Если узел не раскрыт
if( !(p->itemNew.state & TVIS_EXPANDED))
//====== тормозим перерисовку
SetRedraw(FALSE); *pResult = 0;
}
Бит состояния TVIS_EXPANDED не равен нулю, когда узел уже раскрыт. Мы хотим выделить обратный случай, поэтому пользуемся операцией логического отрицания. Метод cwnd:: SetRedraw позволяет установить флаг перерисовки. Если он снят, то система не будет перерисовывать содержимое окна. Вставьте изменения В тело функции обработки Onltemexpanded:
void CLeftView::OnItemexpanded (NMHDR* pNMHDR, LRESULT* pResult) {
NMJTREEVIEW* p = (NMJTREEVIEW*)pNMHDR;
//====== Создаем курсор ожидания
CWaitCursor wait;
//====== Признак раскрытия узла (а не его закрытия)
if (p->itemNew.state & TVIS_EXPANDED)
{
// Описатели раскрываемого и 1-го вложенного узла
HTREEITEM hCur = p->itemNew.hltem,
h = m_Tree.GetChildItem(hCur);
//====== Если имя вложенного узла пусто,
//====== то ветвь еще не раскрывалась
if (m_Tree.GetItemText(h) == "")
{
//====== Удаляем муляж
m_Tree.DeleteItem(h);
//====== Вычисляем полный путь
CString s = GetPath(hCur) + "*.*";
//====== Наполнение раскрытой ветви
CFileFind cff; BOOL bFound = cff.FindFile(s);
while (bFound) {
bFound = cff.FindNextFile();
if (cff.IsDirectory() && !cff.IsDots())
AddItem(hCur, cff.GetFilePath ()); } }
//====== Разрешаем перерисовку
SetRedraw(TRUE); }
*pResult = 0;
}
Здесь реализованы два варианта развития событий: узел открывается впервые и узел отрывается повторно. Признаком первого варианта является наличие пустого элемента с нулевым индексом изображения и пустой строкой текста внутри раскрываемой ветви. Мы удаляем такой элемент, определяем полный файловый путь раскрываемого узла (папки или диска), сканируем файловый адрес и наполняем дерево новыми элементами. Алгоритм заполнения содержимого папки сходен с алгоритмом заполнения логического диска. Также воспользуемся пустым узлом для пометки папок, которые имеет смысл раскрывать, так как в них есть вложенные папки или файлы. Функция GetPath должна пройти вверх по иерархической структуре дерева и вычислить полный файловый путь узла, заданного параметром. Введите коды этой функции в файл LeftView.cpp:
CString CLeftView::GetPath (HTREEITEM hCur)
{
//====== Вычисляет полный файловый путь узла hCur
CString s = "";
for (HTREEITEM h=hCur; h; h=m_Tree.GetParentItem(h))
s = m_Tree.GetItemText(h) + '\\' + s;
return s; }
Размеры левого окна были заданы в момент создания стартовой заготовки и они, пожалуй, маловаты. Исправьте начальные размеры окна, которые задаются при вызове CreateView внутри функции CTreeFrame::OnCreateClient. Посмотрите справку по этой функции и задайте горизонтальный размер окна равным 200.
Запустите приложение и протестируйте работу дерева. Теперь его поведение должно соответствовать тем требованиям, которые были сформулированы в начале разработки проекта. В такие моменты полезно провести эксперименты, чтобы лучше уяснить смысл некоторых действий. Например, временно уберите битовый флаг SHGFI_SMALLICON при вызове SHGetFileinf о и посмотрите, как изменится вид узлов дерева. Затем временно исключите вызов функции SetRedraw в обработчике Onitemexpanding и пронаблюдайте поведение дерева при раскрытии папки, содержащей большое количество вложенных объектов, например winNT.
  
Реакция на выбор узла дерева
Поиск «своих» файлов, то есть файлов с расширением mgn, и демонстрацию их содержимого в виде окон с рисунками следует производить в ответ на выбор (selection) пользователем одного из объектов файлового дерева. Это действие отличается от раскрытия узла дерева, когда пользователь однократно нажимает на маркер (+) раскрытия или делает двойной щелчок на самом узле. Для того чтобы выбрать тот или иной узел, пользователь либо щелкает мышью изображение, либо текстовую строку, соответствующую данному узлу. Реакцию на уведомление об этом событии (OnSelchanged) мы уже ввели в.состав класса CLeftview. Теперь введите внутрь этой функции следующие коды:
void CLeftView::OnSelchanged (NMHDR *pNMHDR, LRESULT *pResult)
{
NM_TREEVIEW* p = (NM_TREEVIEW*)pNMHDR;
//====== Освобождение контейнера текущих файлов
GetDocument()->FreeDocs();
//====== Поиск нужных файлов
SearchForDocs (GetPath(p->itemNew.hItem));
//====== Генерация картинок и демонстрация их в окне
//====== правого представления
GetDocument()->ProcessDocs();
*pResult = 0;
}
Схема обработки сообщения =TVN_SELCHANGED такая же — WM_NOTIFY, но алгоритм отличается. Акцент в обработке переносится в класс документа. Там следует хранить данные о файлах документов, обнаруженных в выбранной папке или на логическом диске, туда же следует ввести новые методы: FreeDocs и ProcessDocs. При изменении выбора пользователя мы:

  • уничтожаем предыдущие данные с помощью метода FreeDocs;
  • ищем и запоминаем свои файлы в выбранном объекте файловой системы (метод SearchForDocs);
  • создаем новые объекты вспомогательного класса CPolygon;
  • просим их прочесть свои данные в обнаруженных файлах;
  • отображаем их в виде картинок в окне правого представления с помощью метода ProcessDocs.

Поиск документов своего типа (mgn-файлов) производится по той же схеме с использованием класса CFindFile и его методов поиска объектов файловой системы. Но если ранее мы просматривали все объекты, задав маску поиска "*.*", то теперь мы можем сузить поиск, задав маску "* .mgn". Полные пути найденных файлов будем хранить в контейнере m_sFiles типа vector<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());
}

Класс 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; // Пока не знаем обратного адреса
MakeStar(); // Зададим полигон в виде звезды
}

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г. Яндекс.Метрика