Управление
файловым деревом
В этом уроке мы подробно рассмотрим процесс разработки MDI-приложения, в котором один тип документов взаимодействует с несколькими своими представлениями. В рамках архитектуры «документ — представление» принято использовать следующие термины:
Главным моментом в архитектуре является то, что один документ может иметь несколько связанных с ним представлений, но каждое из них может быть связано лишь с одним документом.
Особенностью разрабатываемого приложения является то, что в одном из представлений, управляемых классом cscrollview, пользователь сможет просматривать в качестве «картинок» — чертежей или схем, выбирать и открывать документы своего приложения, которые расположены в файлах с различными адресами. Навигацию по файловому дереву будем осуществлять с помощью второго представления, которым управляет класс CTreeView. Классы CScrollView и CTreeView являются специализированными потомками класса cview. Класс CTreeView тесно связан с классом CTreeCtrl, который разработан как элемент управления произвольным деревом. Мы должны научиться им управлять.
Документ, выбранный пользователем с помощью двух предыдущих представлений, отображается в третьем, производном от cview, которое служит посредником между пользователем и данными документа. В его окне пользователь сможет редактировать данные документа. В качестве данных мы используем динамический массив (контейнер) точек с вещественными координатами, который удачно моделирует произвольный чертеж — двухмерную проекцию какого-либо элемента конструкции. Идеи, заложенные в этом учебном приложении, использованы в реальном проекте по расчету физических полей, описываемых дифференциальными уравнениями в частных производных. В частности, производились расчеты поля магнитов, отсюда проистекает выбранное нами расширение (mgn) для документов приложения. В задачах такого рода исходными являются данные о геометрии расчетной области. Именно она наиболее точно определяет документ (вариант расчета). Если число таких геометрий велико, то поиск варианта по картинке расчетной области существенно упрощает жизнь исследователя физических полей. В связи с этим был получен заказ — ввести в проект возможность поиска и выбора документа по миниатюрному графическому представлению (схеме) геометрии расчетной области. Упрощенная реализация этой части проекта рассмотрена ниже. Начнем с создания стартовой заготовки MDI-приложения.
Просмотрите плоды работы мастера в окне 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 это делается так:
Аббревиатура /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. Для этого:
Повторив эту процедуру столько раз, сколько необходимо иметь различающихся изображений, закончите тем, что последнему из них присвойте 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 и вновь посмотреть, что
получится. Обратите внимание на то, что изображения изменяются при выборе узлов,
то есть при переводе курсора мыши с одного узла на другой.
Рис.
5.1. Вид главного окна приложения Tree
Обращаемся
к операционной системе
Теперь, когда
вы научились управлять формой дерева, мы продолжим развитие приложения. Используя
клавишу 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 вставляет узел в дерево. Его параметры задают:
Вставляемый
в дерево логический диск надо проверить на наличие вложенных сущностей и вставить
внутрь данного узла дерева хотя бы один элемент, когда диск не пуст. Если этого
не сделаеть, то в дереве не будет присутствовать маркер (+), с помощью которого
пользователь раскрывает узел.
При проверке
диска (функция 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, определены
на этапе регистрации значка приложения или его документа.
Реакция
на уведомляющие сообщения CTreeCtrl
Когда пользователь
раскрывает узел дерева, то встроенный в класс CTreeView объект класса CTreeCtrl
посылает родительскому окну (нашему представлению CLef tview) уведомляющее сообщение.
Оно работает по схеме WM_NOTIFY, которую мы уже рассматривали. Наш класс CLef
tview должен реагировать на это сообщение, сканировать раскрываемую папку или
логический диск и изменять дерево, вставляя в раскрываемую ветвь новые объекты
файловой системы, которые обнаружены внутри папки или диска. Для того чтобы
ввести в класс способность реагировать на рассматриваемое сообщение, вы должны:
Буква 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. При изменении выбора пользователя
мы:
Поиск документов
своего типа (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());
}
}
В соответствии
с архитектурой «документ — представление» мы должны ввести в класс документа
некоторые новые структуры данных для хранения информации о файлах документов,
обнаруженных в выбранной пайке или логическом диске. Файловые пути хранятся
в контейнере текстовых строк типа 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; // Зададим полигон в виде звезды
}
Важными моментами
в жизни объектов являются те, когда они копируются или создаются на основе уже
существующих. Реализация конструктора копирования объектов просто обязательна,
если вы пользуетесь контейнером объектов. В случае отсутствия или некорректного
тела конструктора контейнеры откажутся работать с объектами класса. Обычным
приемом при этом является реализация в классе операции присвоения operator=
() и последующее ее воспроизведение в конструкторе копирования. Обратите внимание
на тип возвращаемого значения операции присвоения. Это ссылка (CPolygon&)
на активный или стоящий в левой части операции присвоения — *this, объект класса:
CPolygoni CPolygon::operator=(const
CPolygonS poly){
//====== Копируем
все данные
m_pDoc = poly.m_pDoc;
m_nPenWidth =
poly.m_nPenWidth;
m_PenColor =
poly.m_PenColor;
m_BrushColor
= poly.m_BrushColor;
m_ptLT = poly.m_ptLT;
m_ptRB = poly.m_ptRB;
//===== Освобождаем
контейнер точек
if (!m_Points.empty())
m_Points.clear();
//====== Копируем
все точки. Возможно решение с помощью assign.
for (OINT i=0; i<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);
}
Задание координат полигонов является утомительным занятием, поэтому мы, учитывая учебный характер приложения, создали три вспомогательные функции, которые позволяют быстро воспроизвести три различных полигона: звезду, треугольник и пятиугольник. Далее нам необходим немодальный диалог, с помощью которого пользователь сможет создать произвольное количество новых полигонов, выбирая их типы с помощью нестандартных кнопок и управляя атрибутами полигонов (цветом фона, цветом и толщиной пера) с помощью синхронизированных между собой элементов управления. Дополните файл ТгееОос.срр кодами еще трех функций:
void CPolygon::MakeStar()
{
m_Points.clear();
//====== Вспомогательные
переменные
double pi = 4. * atan(l.), // Углы
al = pi / 10.,
а2 = 3. * al,
//====== 2 характерные
точки
xl = cos (al),
yl = sin(al),
x2 = cos(a2),
y2 = sin(a2);
//=== Вещественные (World) координаты углов звезды m_Points.push_back(CDPoint(0., 1.));
m_Points.push_back(CDPoint <-x2, -y2));
m_Points.push_back(CDPoint( xl, yl) ) ;
m_Points.push_back(CDPoint(-xl, yl)) ;
m_Points.push_back(CDPoint(
x2, -y2));
//====== Габариты
звезды
m_ptLT = CDPoint(-xl, 1.);
m_ptRB = CDPoint(
xl,-y2);
//====== Генерация треугольника
void CPolygon::MakeTria()
{
m_Points.clear();
double pi = 4. * atand(1.);
a = pi / 6.;
x = cos (a) ;
у = sin(a);
m_Points.push_back (CDPoint(0., 1.));
m_Points,push_back (CDPoint(-x, -y) );
m_Points.push_back (CDPoint( x, -y));
m_ptLT = CDPoint (-x, 1.) ;
m_ptRB = CDPoint
( x,-y);
//====== Генерация
пятиугольника
void CPolygon::MakePent()
{
m_Points.clear
();
double pi
= 4. * atan(l.),
al = pi / 10.,
a2 - 3. * al,
xl = cos(al),
yl = sin(al),
x2 = cos(a2),
y2 = sin(a2);
// Вещественные (World) координаты углов пятиугольника m_Points.push_back(CDPoint (0 ., 1.));
m_Points.push_back(CDPoint(-xl, yl));
m_Points.push_back(CDPoint(-x2, -y2));
m_Points.push_back(CDPoint( x2, -y2));
m_Points.push_back(CDPoint(
xl, yl));
m_ptLT = CDPoint(-xl, 1.);
m_ptRB = CDPoint(
xl,-y2);
Теперь, когда мы имеем вспомогательные классы (CDPoint и CPolygon), можно подумать о структуре данных класса CTreeDoc. Нам понадобятся:
Кроме этого,
нам понадобятся методы для управления тремя окнами: 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, то вы можете запустить приложение,
с тем чтобы устранить возможные ошибки. Но пока никакой новой функциональности
оно не обнаружит, так как мы еще не занимались созданием и управлением других
представлений его документа. Нам вновь придется вернуться к классу документ,
но только после того, как будут разработаны классы связанных с ним представлений.
Библиотека
MFC предоставляет 3 различных способа создания окон-рамок, обрамляющих представления.
Два явных способа: вызов методов CFrameWnd:: Create или CFrameWnd:: LoadFrame
и один неявный — путем создания шаблона документа. Класс MFC-приложения в рамках
многодокументного интерфейса (MDI) поддерживает динамический список шаблонов
документов, то есть объектов класса CMultiDocTemplate. Каждый из них управляет
оркестром из четырех музыкантов: ресурсы, документ, связаное с ним представление
и обрамляющее его окно-рамка. Этот квартет составляется в момент создания нового
объекта CMultiDocTemplate внутри метода initinstance класса приложения, в нашем
случае СТгееАрр. Найдите это место в программе и убедитесь, что параметрами
конструктора являются перечисленные музыканты:
CMultiDocTemplate*
pDocTemplate;
//====== Создание
шаблона документов
pDocTemplate
= new CMultiDocTemplate(IDR_TreeTYPE,
RUNTIME_CLASS(CTreeDoc),
// документ
RUNTIME_CLASS(CTreeFrame),
// окно-рамка
RUNTIME CLASS(CLeftView));
// представление
//====== Вставка
адреса шаблона в динамический список
AddDocTemplate(pDocTemplate);
Мы можем при
желании поменять состав квартета, но должны оставить неизменным количество его
исполнителей. В наши цели входит создание еще одной новой комбинации. Тот же
документ будет связан с другими ресурсами I DR_DrawTYPE, другим представлением
(CDrawView) и другой рамкой (CDrawFrame). Так как мы хотим управлять обоими
квартетами, то удобно запомнить в классе приложения их адреса и пользоваться
ими при необходимости создать новое или активизировать существующее окно MDI-документа.
Введите в состав класса CTreeApp две public-переменные, которые будут хранить
эти адреса. С учетом незначительных сокращений интерфейс класса должен иметь
такой вид:
class
CTreeApp : public CWinApp
{
public:
//====== Два
шаблона документов
CMultiDocTemplate
*m_pTemplDraw; CMultiDocTemplate *m_pTemplTree;
CTreeApp () ;
virtual BOOL Initlnstance();
afx_msg void OnAppAbout();
DECLARE_MESSAGE_MAP()
};
Внесем некоторые
изменения и сокращения в файл реализации класса CTreeApp. Откройте файл Тгее.срр
в окне редактора и просмотрите коды функции Initlnstance. Если у вас присутствует
блок кодов
if (lAfxOlelnitО)
{
AfxMessageBox(IDP_OLE_INIT_FAILED)
;
return FALSE;
}
AfxEnableControlContainer() ;
который представляет
собой инициализацию поддержки OLE (Object Linking and Embedding), то его можно
убрать, так как наше приложение не будет выполнять функции OLE-сервера или OLE-контейнера.
Следующая строка:
SetRegistryKey(_T("Local AppWizard-Generated..."));
представляет
собой создание нового ключа в реестре Windows для хранения некоторой информации
о нашем приложении. Он действительно будет новым, если вы измените строку текста
на имя вашей компании, как это было задумано при разработке функции, или на
какое либо другое имя («My Soft»). После запуска приложения можно открыть реестр
(в командной строке Windows дайте команду: RegEdit) и отыскать в нем по адресу
HKEY_CURRENT_USER\Software вновь созданный ключ My Soft. Записывать информацию
по этому ключу вы можете с помощью методов класса cwinApp, от которого происходит
наш класс CTreeApp. Например, метод WriteProf ilelnt позволяет записать некое
целое значение (value), соответствующее произвольной секции текущего ключа.
Для эксперимента вставьте вместо строки SetRegistryKey такие три строки:
SetRegistryKey("My
Soft");
WriteProfileStringC'My Data", "My Name","Alex");
WriteProfilelnt("My
Data","My Age",54);
Запустите
приложение, перейдите в окно реестра, обновите его (View > Refresh), найдите
адрес HKEY_CURRENT_USER\Software\My Soft\Tree\My Data, поставьте в него курсор
мыши и убедитесь в наличии двух записей, высвечиваемых в правом окне реестра.
Удалите из реестра ключ My Soft, если вам нужен реестр, а не свалка мусора (чем
он обычно и является). Уберите также учебный код из тела initinstance.
Для того чтобы
увидеть, как работает функция LoadStdProf ileSettings, вызов которой виден в
теле initinstance, запустите приложение и запишите хотя бы один документ (команда:
File > Save). После этого вы можете найти в реестре (не забывайте освежать
его) по тому же адресу новую секцию Recent File List, которая содержит запись
— полный путь к только что записанному файлу. Параметр функции LoadStdProf ileSettings
указывает, сколько записей может содержать список MRU (Most Recently Used) последних
документов. Если вы зададите его равным нулю, то список не будет поддерживаться
каркасом приложения.
Теперь можно
приступить к созданию двух шаблонов документов вместо одного, рассмотренного
выше. Для того чтобы задействовать второй шаблон, надо убрать из Initinstance
код по созданию шаблона pDocTemplate и вставить вместо него такие строки:
//====== Создаем
первый шаблон
m_pTemplTree
= new CMultiDocTemplate(IDR_TreeTYPE,
RUNTIME_CLASS(CTreeDoc)
,
RUNTIME_CLASS(CTreeFrame)
,
RUNTIME_CLASS(CLeftView))
;
//====== Помещаем
его в список
AddDocTemplate(m_pTemplTree);
//====== Создаем
второй шаблон
m_pTemplDraw
= new CMultiDocTemplate(IDR_DrawTYPE,
RUNTIME_CLASS(CTreeDoc),
RUNTIME_CLASS(CDrawFrame),
RUNTIME_CLASS(CDrawView));
//====== Помещаем
его в список
AddDocTemplate(m_pTemplDraw);
Второй шаблон
тоже помещается в список шаблонов приложения. Каркас приложения устроен так,
что теперь каждый раз, когда пользователь будет выбирать команду File > New,
будет появляться диалог со списком шаблонов и просить его выбрать шаблон, которому
должен соответствовать новый документ. Идентификатор ресурсов !DR_DrawTYPE определяется
заранее, то есть в файле resource.h должна
быть макроподстановка #def ine, заменяющая этот идентификатор целым положительным
числом. Самым простым способом создания нового идентификатора является вызов
команды Edit > Resource Symbols. Но этот способ будет некорректным
в нашем случае, так как мы поместили второй шаблон в список шаблонов, ассоциированных
с документами приложения, и его идентификатор должен быть связан с какими-то
ресурсами.
Ресурсов,
которые связаны со вторым шаблоном, может быть несколько, и мы покажем, как
связать с ним значок, меню, панель инструментов и строковый ресурс, расположенный
в таблице String Table. Последний является текстовой строкой, которая разбита
символами ' \п' на отдельные части — подстроки. Каждая подстрока имеет определенное
значение и используется каркасом приложения в разные моменты его жизни. Например,
вторая подстрока является корнем для образования имен новых документов, и вы
обычно видите ее в заголовке дочернего окна документа. Откройте окно Resource
View, раскройте узел дерева под именем String Table и сделайте двойной щелчок
на вложенном в него элементе. В таблице строк справа найдите iDR_TreeTYPE. Он
идентифицирует комплексную строку:
\nTree\nTree\nTree
Files (*.mgn)\n.mgnXnTree.Document\nTree.Document
Вы
можете получить справку по всем частям этой строки, если вызовете помощь (Help)
по индексу GetDocString — методу класса CDocTemplate, позволяющему выделить
нужную подстроку комплексной строки.
Если мы поместим
в String Table новую строку с идентификатором !DR_DrawTYPE, то при открытии
окон документов по шаблону m_pTemplDraw, они будут использовать этот ресурс.
При вставке новой строки надо быть внимательным, так как ее индекс должен быть
в определенном диапазоне.
Убедитесь в том, что индекс новой строки (видимо, 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);
}
Если мы не
поленимся и создадим для второго шаблона документов все остальные перечисленные
выше ресурсы, то приложение действительно будет вести себя в соответствии с
концепцией MDI, так как она трактуется компанией Microsoft. Это означает, что
приложение будет следить за типом активного документа и автоматически изменять
связанные с ним ресурсы (значок и меню). К сожалению, автоматическая
смена панели инструментов все-таки потребует некоторых усилий. Имя текущего
документа совпадает с именем файла, в котором он хранится, но если документ
еще не был сохранен, то его имя генерируется автоматически и также зависит от
выбранного шаблона. Перейдите в окно Resource View и откройте в дереве ресурсов
узел Icon. Вы видите индексы двух значков, которые сгенерированы мастером AppWi
zard и служат для идентификации приложения (IDR_MA IN FRAME) и его документов
(IDR_TreeTYPE). При желании вы можете отредактировать изображения, открыв их
в окне редактора или, что проще, заменив их на другие. Техника замены проста:
открыть готовое изображение в рамках Studio.Net (их много по адресу ..\Microsoft
Visual Studio.Net \ Common7\ Graphics\icons), скопировать в буфер, открыть существующий
значок, нажать Delete и Ctrl+V. He забывайте, что на самом деле имеется 4 значка
(2 маленьких и 2 больших). Переход между изображениями значков разных размеров
производится в диалоге, вызываемом командой Image > Open > Image >
Туре. Команда доступна, когда курсор стоит в окне редактора изображений. Теперь
опишем, как добавить еще один значок.
Откройте в окне редактора меню IDR_TreeTYPE. Удалите из меню File команды Print, Print Preview, Print Setup и один разделитель (Separator). Повторите действия по копированию ресурсов и сделайте копию всего меню IDR_TreeTYPE. Поменяйте идентификатор копии на IDR_DrawTYPE. Откройте копию меню в окне редактора и уберите из него команду View > Split и добавьте View > Documents с идентификатором ID_VIEW_TOGGLE. В пункт меню Edit добавьте две новые команды: New Poly и Poly Color. ... Вновь откройте меню IDR_TreeTYPE, удалите в нем весь пункт меню Edit, добавьте команды View > Geometry с идентификатором ID_VIEW_TOGGLE и View > Refresh с идентификатором ID_VIEW_REFRESH. Команда ID_VIEW_70GGLE будет служить для переключения между двумя окнами CTreeFrame и CDrawFrame, содержащими три представления одного и того же документа.
В том и другом из рассматриваемых меню измените команду Window > Tile на Window > Tile Horizontally и добавьте новую команду Window > Tile Vertically, выбрав из выпадающего списка идентификатор ID_WINDOW_TILE_VERT. Обработчики этих команд уже существуют в каркасе приложения, поэтому нам не нужно их создавать.
Откройте инструментальную панель (Toolbar) IDR_MAINFRAME. Удалите из нее кнопки вырезания, копирования, вставки и печати. Используя технику копирования, добавьте две новые инструментальные панели: IDR_TreeTYPE и IDR_DrawTYPE. В последнюю вставьте две новые кнопки, соответствующие двум командам меню: New Poly и Poly Color.. .. Добавьте два новых курсора, которые будут использованы в разные моменты жизни приложения. Один будет загружаться автоматически при попадании фокуса в окна демонстрации содержимого документов (типа CWndGeom). Другой будет использован в режиме визуального редактирования данных документа.
Если вы вместо IDC_MYHAND зададите IDC_HAND, то компилятор многократно сообщит о переопределении стандартного курсора с таким индексом. Внесите еще одно изменение, связанное с ресурсами. Так как мы сами будем перезагружать инструментальные панели, то надо упростить доступ к ним. В файле MainFrm.h перенесите следующие два объявления из секции protected в секцию public:
public:
CToolBar m_wndToolBar;
CStatusBar m_wndStatusBar;
Класс для нового представления документа
При создании
второго шаблона документов мы определили новую комбинацию классов, которые будут
поддерживать функционирование окон нового типа. Клиентской областью этих окон
будет управлять класс CDrawView, к созданию которого мы и приступаем.
Среди
классов списка Base Class теперь есть класс CObject. Как ни странно, но в Visual
Studio 6 вы не могли бы рассчитывать на помощь ClassWizard при создании класса,
производного от CObject.
В папке проекта появились еще два файла (DrawView.h и DrawView.cpp), которые были автоматически включены в состав проекта. Класс приложения должен их видеть, поэтому вставьте в конец списка директив #include файла Тгее.срр еще одну:
#include
"DrawView.h"
Внесите изменения
в интерфейс класса, так чтобы он стал:
#pragma
once
class CTreeDoc;
// Упреждающее объявление
class CDrawView
: public CView {
DECLARE_DYNCREATE(CDrawView)
protected:
CSize m_szView;
// Размеры клиетской области окна
bool m_bNewPoints;
// Флаг режима вставки новых точек
bool m_bReady;
// Флаг готовности захвата вершины
bool m_bLock;
// Флаг захвата вершины
int m_CurID;
// Индекс полигона в массиве
HCURSOR m_hGrab;
// Курсор захвата
CPen m_penLine;
// Перо для изображения контура
CDrawView();
virtual ~CDrawView();
public:
CTreeDoc* GetDocument()
{
return dynamic_cast<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. Переводя фокус из одного окна в другое, обратите внимание на смену строк меню главного окна. Значки в верхнем левом углу окон документов тоже должны быть разными. Панели инструментов, как мы уже отмечали, автоматически не изменяются. Эту функциональность мы внесем позже.
Класс для просмотра изображений
Класс представления документа CRightView служит для иллюстрации содержимого всех документов, обнаруженных в текущей выбранной папке. В окне CRightView мы рядами и столбцами разместим другие простые окна, управляемые классом CWndGeom, которые будут иметь одинаковый размер и изображать геометрию конструкции, соответствующей данным документа. Причем изображение в контексте окна воспроизведут сами документы, точнее объекты m_poly, которые есть в каждом из них. Далее окна класса CWndGeom мы будем называть картинками.
Так как количество
документов в текущей папке произвольно и заранее не известно (но они все должны
быть доступны пользователю), то, чтобы разместить все картинки, размеры окна
CRightView должны быть переменными. Окно должно быть «резиновым». Класс CRightView
был изначально создан мастером AppWizard как класс, способный прокручивать содержимое
своего окна, так как в качестве базового класса для него был выбран csroliview.
Благодаря этому класс приобрел
способность следить за размерами своего окна и при необходимости создавать полосы
горизонтальной и вертикальной прокрутки. Наша цель — научиться программно управлять
размерами окна прокрутки, динамически создавать и уничтожать окна картинок и
правильно изображать в них геометрию конструкции, опираясь на данные документа.
Скорректируйте коды стартовой заготовки с интерфейсом класса так, как показано
ниже:
#pragma
once
//====== Класс
для демонстрации содержимого документов
class CRightView
: public CScrollView {
//====== Упреждающее
объявление класса картинок
friend class
CWndGeom; protected:
CSize m_szView;
// Реальные размеры окна
CSize m_szScroll;
// Размеры прокручиваемого окна
CSize m_szltem;
// Размеры картинки
CSize m_szMargin;
// Размеры полей
CString m_WndClass;
// Строка регистрации картинки
CRightView ()
;
DECLARE_DYNCREATE(CRightView)
public: //====== Контейнер картинок
vector<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, которая должна:
Дальше события
развиваются автоматически. После создания окна 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 () ;
}
Характерный
для MFC двухступенчатый способ создания окна cwndGeom объясняется тем, что с
каждым окном связаны две сущности: Windows-окно, характеризуемое описателем
окна, и объект класса cwndGeom, который мы еще должны разработать. В коде функции
show для каждого полигона сначала динамически создается объект класса cwndGeom
(конструктор класса), а затем — управляемое им Windows-окно (Create). При создании
объекта мы передаем ему указатель на класс родительского окна и индекс полигона
в контейнере. Поэтому окно впоследствии сможет найти нужный полигон в документе
и изобразить его в своем контексте. Мы запоминаем адреса всех объектов
CwndGeom в массиве m_pWnds, для
того чтобы потом можно было уничтожить все Windows-окна (вызвав DestroyWindow),
так же, как и все объекты класса cwndGeom (вызвав деструктор класса CWndGeom).
Эту процедуру надо выполнять каждый раз, когда пользователь выбирает новый узел
в файловом дереве.
Вам уже знакома
процедура ввода в проект новых классов. Сейчас настала пора применить ее для
ввода в проект класса cwndGeom. При работе с мастером MFC Class Wizard выберите
в качестве базового класс cwnd и измените предлагаемые по умолчанию имена файлов,
в которых будут размещены стартовые коды нового класса. Вместо WndGeom.h и WndGeom.cpp
задайте RightView.h и RightView.cpp. После того как мастер закончит работу,
вставьте в начало файла. RightView.h упреждающее объявление class CWndGeom;
так как класс CRightview содержит массив указателей этого типа, а его объявление
стоит до объявления cwndGeom.
Надо
отметить, что в бета-версии Studio.Net описываемый способ размещения нового
класса работает неверно и для исправления ситуации мне пришлось убрать вновь
добавленную директиву #pragma once из файла RightView.h и три новые, вставленные
мастером, директивы ^include из файла RightView.cpp. Надеюсь, что у вас ошибок
такого рода не будет.
Изготовленная
мастером заготовка класса содержит несколько больше элементов, чем нам необходимо.
В частности, не нужны макросы DECLARE_DYNAMIC и IMPLEMENT^ DYNAMIC, так как
мы не собираемся использовать унаследованную от CObject функцию isKindOf. Посмотрите
справку по концепции наследования от CObject, чтобы понять, как связаны макросы
с функцией IsKindOf, затем уберите макросы и внесите изменения в интерфейс класса
так, чтобы он был:
class
CWndGeom : public CWnd
{
public:
CTreeDoc *m_pDoc;
// Адрес документа (для удобства)
CRightview *m_pView;
// Адрес родительского окна
int m_ID;
// Индекс окна документа в массиве CRect m_Rect;
// Координаты
в правом окне
//====== Удобный
для нас конструктор
CWndGeom (CRightview *p, int id);
~CWndGeom();
protected:
DECLARE_MESSAGE_MAP()
};
В файле реализации
класса измените коды конструктора, как показано ниже. Затем с помощью Studio.Net
введите в класс реакции на следующие сообщения: WM_PAINT, WM_LBUTTONDOWN и WM_MOUSEMOVE.
Цель этих действий такова. При наведении курсора мыши на одно из окон, управляемых
классом CWndGeom, оно должно проявить признаки готовности быть выбранным. Для
этого рисуем в нем обрамляющий прямоугольник, который исчезает при выходе указателя
мыши за пределы окна. Эта функциональность реализуется за счет пары функций
SetCapture
- ReleaseCapture. Метод CWnd: : SetCapture захватывает текущее окно как адресат
для последующих сообщений мыши независимо от позиции курсора. Поэтому при перемещении
курсора мыши можно выйти из пределов клиентской области окна и все равно получить
и обработать сообщение им_ MOUSEMOVE. На этом свойстве окна и построен алгоритм
его подсветки. Функция ReleaseCapture «освобождает мышь», то есть вновь восстанавливает
обычный порядок обработки мышиных сообщений. Мы вызываем функцию после того,
как обнаружен факт выхода за пределы окна и снята подсветка, то есть стерт обрамляющий
прямоугольник:
CWndGeom::CWndGeom(CRightView *p, int id)
{
//====== Запоминаем
адрес родительского окна
m_pView = р;
//====== Запоминаем
адрес документа
m_pDoc = p->GetDocument();
//====== и индекс
окна в массиве
m_ID = id;
}
void CWndGeom::OnPaint()
{
CPaintDC dc(this);
dc.SetMapMode(MM_ISOTROPIC)
;
//====== Настраиваем
логическое окно
dc.SetWindowOrg
(m_pDoc->m_szDoc.cx/2, m_pDoc->m_szDoc.cy/2), dc.SetWindowExt(m_pDoc->m_szDoc);
//====== Узнаем
текущие размеры окна
GetClientRect(&m_Rect);
int w
= m_Rect.Width (), h = m_Rect.Height ();
//====== Настраиваем
аппаратное окно
dc.SetViewportOrg (w/2, h/2);
dc.SetViewportExt
(w, -h);
//=== Выбираем в контейнере нужный полигон и просим
//=== его изобразить себя в подготовленном контексте m_pDoc->m_Shapes[m_ID].Draw(Sdc);
}
void CWndGeom: :OnLButtonDown (UINT nFlags, CPoint point)
{
//====== Изменяем
дежурный полигон
m_pDoc->m_Poly
= m_pDoc->m_Shapes [m_ID] ;
//== Если не было CDrawView, то создаем его
if (m_pDoc->MakeView() )
return;
//=== Если он
был, то находим его и делаем активным
CView *pView = m_pDoc->GetView (RUNTIME_CLASS (CDrawView) ;
{
(CMDIChildWnd*)pView->GetParentFrame ()
} ->MDIActivate
() ;
//====== Перерисовка
с учетом изменений
pView->Invalidate
( ) ;
Все «мышиные»
сообщения сопровождаются параметрами, которые информируют нас о том, где и как
произошло событие. Первый параметр есть набор битов, указывающих, какие виртуальные
клавиши были нажаты в момент события. Например, если nFlags содержит бит с именем
MK_CONTROL (символическая константа), то это означает, что в момент нажатия
левой кнопки мыши также была нажата клавиша Ctrl. Второй параметр содержит координаты
(х, у) местоположения курсора в момент события. Они заданы относительно верхнего
левого угла клиентской области окна:
void CWndGeom: lOnMouseMove (UINT nFlags, CPoint point)
{
//====== Если указатель мыши в пределах окна,
if (m_Rect.Pt!nRect (point))
{
//====== то захватываем
мышь, выбираем перо
//====== и рисуем
обрамляющий прямоугольник
SetCapture ()
;
CClientDC dc(this)
;
CPen pen (PS_SOLID,
4, RGB (192, 192, 255));
dc.SelectObject
(&pen);
dc.MoveTo(m_Rect.left+4, m_Rect . top+4) ;
dc.LineTo (m_Rect.right-4, m_Rect . top+4) ;
dc.LineTo (m_Rect.right-4, m_Rect .bottom-4) ;
dc.LineTo (m_Rect.left+4, m_Rect .bottom-4) ;
dc.LineTo (m_Rect.left+4
, m_Rect . top+4) ;
}
else
{
ReleaseCapture () ;
// Освобождаем мышь Invalidated;
// Прямоугольник будет стерт
}
}
Так как в
коде функции OnLButtonDown содержится обращение к объекту класса CDrawView,
то необходимо в список директив препроцессора текущего файла (RightView.cpp)
вставить еще одну: #include "DrawView.h".
Взаимодействие
представлений документа
В данный момент
мы имеем три класса (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 поступает при инициализации левого окна.
Визуальное редактирование данных
Несмотря на
то что разрабатываемое приложение носит учебный характер, оно моделирует вполне
реальные ситуации, когда путем навигации по дереву файлов пользователь ищет
и выбирает документ, для того чтобы открыть его в окне представления, специализированного
для внесения изменений в данные. В отличие от
Windows Explorer
мы даем возможность пользователю выбрать документ не по его имени и значку,
а по его содержимому в виде чертежа конструкции.
Современным
подходом к редактированию данных является использование таблиц (grids) типа
Excel, в которых отражены данные открытого документа и которые позволяют редактировать
их, мгновенно получая обратную связь в виде изменившейся геометрии устройства.
Таблицы удобно разместить на одной из панелей расщепленного окна с регулируемой
перегородкой (split bar).
К сожалению,
в MFC нет классов, поддерживающих функционирование таблиц. Реализация их в виде
внедряемых СОМ-объектов обладает рядом недостатков. Во-первых, существующие
grid-элементы обладают весьма ограниченными возможностями. Во-вторых, интерфейсы
обмена данными между внедренной (embedded) таблицей и приложением-контейнером
громоздки и неуклюжи. Самым лучшим, известным автору, решением этой проблемы
является использование библиотеки классов objective Grids, разработанных компанией
stingray Software. Библиотека полностью совместима с MFC. В ней есть множество
классов, поддерживающих работу разнообразных элементов управления: combo box,
check box, radio button, spinner, progress и др. Управление grid-элементами
или окнами типа CGXGridWnd на уровне исходных кодов дает полную свободу в воплощении
замыслов разработчика.
Однако, не
имея лицензии на использование данного продукта, я не могу использовать его
в разработке даже этого учебного приложения. Поэтому мы пойдем традиционным
путем и внесем в проект возможность визуального редактирования данных с помощью
обычных мышиных манипуляций. Представление, поддерживаемое классом CDrawView,
как было уже отмечено, должено служить посредником между пользователем и данными
текущего полигона.
Изменение
координат вершин полигона в диапазоне, ограниченном размерами логической области
(2000x2000), можно производить простым перетаскиванием его вершин с помощью
указателя мыши. Чтобы намекнуть пользователю нашего приложения о возможности
произведения таких операций (вряд ли он будет читать инструкцию), мы используем
стандартный прием, заключающийся в изменении формы курсора в те моменты, когда
указатель мыши находится вблизи характерных точек изображения. Это те точки,
которые можно перетаскивать. В нашем случае — вершины полигона. Очевидной реакцией
на курсор в виде четырех перекрещенных стрелок является нажатие левой кнопки
и начало перетаскивания. Заканчивают перетаскивание либо отпусканием кнопки
мыши, либо повторным ее нажатием. Во втором варианте при перетаскивании не обязательно
держать кнопку нажатой. Остановимся именно на нем.
В процессе
перемещения можно постоянно перерисовывать весь объект, что обычно сопровождается
неприятным мельканием, а можно пользоваться приемом, сходным с технологией rubber-band
(резиновая лента). Вы используете ее, когда выделяете несколько объектов на
рабочем столе Windows. Прием характеризуется упрощенной перерисовкой контура
перемещаемого объекта. При этом объект обыч-
но обесцвечивается.
Такую функциональность мы уже ввели в класс CPolygon. Тонким местом в этой технологии
является особый режим рисования линий контура. Каждое положение перемещаемой
линии рисуется дважды. Первый раз линия рисуется, второй — стирается. Этот эффект
достигается благодаря предварительной настройке контекста устройства, которую
производит функция SetROP2. Если вызвать ее с параметром R2_xoRPEN, то рисование
будет происходить по законам логической операции XOR (исключающее ИЛИ). В булевой
алгебре эта операция имеет еще одно имя — сложение по модулю два. Законы эти
просты: 0+0=0; 0+1 = 1; 1+0=1; 1 + 1=0. Ситуацию повторного рисования можно
представить так:
Итак, повторный
проход стирает линию. В качестве упражнения повторите выкладки при условии,
что перо белое (затем — черное). Такие упражнения шлифуют самое главное качество
программиста — упорство. При черном пере вы должны получить что-то не то. Тем
не менее мы берем черное перо, но при этом задаем стиль PS_DOT, что в принципе
равносильно черно-белому перу. Белые участки работают как описано, а черные
своей инертностью помогают создать довольно интересный эффект переливания пунктира
или эффект натягивания и сжимания резинки. Есть еще одно значение (К2_ыот) параметра
функции SetROP2, которое работает успешно, но не без эффекта резинки.
Я думаю, что цифра 2 в имени функции означает намек на фонетическую близость английских слов «two» и «to». Если предположение верно, то имя функции SetROP2 можно прочесть как «Set Raster Operation To», что имеет смысл установки режима растровой операции в положение (значение), заданное параметром функции. Обязательно просмотрите справку по этой функции (методу класса CDC), для того чтобы узнать ваши возможности при выборе конкретного режима рисования.
Режим перетаскивания
вершин полигона готов к использованию в момент вхождения указателя мыши в область
чувствительности вершины (за этим следит флаг m_bReady). Кроме данного режима
мы реализуем еще один режим — режим создания нового полигона (флаг m_bNewPoints),
который вступает в действие при выборе команды меню Edit > New Poly. При
анализе кода обратите внимание на то, что мы получаем от системы координаты
точек в аппаратной системе, а запоминать
в контейнере точек должны мировые (World) координаты. Преобразование координат
осуществляется в два этапа:
Теперь вы,
вероятно, подготовлены к восприятию того, что происходит в следующих трех методах
класса CDrawView. Первые два вы должны создать как реакции на сообщения WM_LBUTTONDOWN
и WM_MOUSEMOVE, а последний (member function) — просто поместить в файл реализации
класса, так как его прототип уже существует:
void CDrawView::OnLButtonDown(UINT nFlags, CPoint point)
{
//======
В режиме создания нового полигона
if (m_bNewPoints)
{
CTreeDoc *pDoc
= GetDocument();
//====== Ссылка
на массив точек текущего полигона
VECPTSS pts =
pDoc->m_Poly.m_Points;
//=== Получаем
адрес текущего контекста устройства
CDC *pDC = GetDC()
;
//====== Настраиваем
его с учетом размеров окна
SetDC(pDC) ;
//=== Преобразуем аппаратные координаты в логические
pDC->DPtoLP(ipoint);
//=== Преобразуем Page-координаты в World-координаты
CDPoint pt = pDoc->MapToWorldPt(point);
//====== Запоминаем
в контейнере
pts.push_back (pt);
}
//====== В режиме
готовности к захвату
else if (m_bReady)
{
ra_bLock = true;
// Запоминаем состояние захвата
m_bReady = false; // Снимаем флаг готовности
}
//====== В режиме
повторного нажатия
else if (mJbLock)
m_bLock = false; // Снимаем флаг захвата
else
//В случае бездумного
нажатия
return; //
уходим
Invalidated; // Просим перерисовать
}
void CDrawView::OnMouseMove(UINT nFlags, CPoint point)
{
//=== В режиме
создания нового полигона не участвуем
if (m_bNewPoints)
return;
//====== Получаем
и настраиваем контекст
CDC *pDC = GetDCO ;
SetDC(pDC);
//=== Преобразуем аппаратные координаты в логические
pDC->DPtoLP(Spoint);
//=== Преобразуем Page-координаты в World-координаты
CTreeDoc *pDoc = GetDocument();
CDPoint pt =
pDoc->MapToWorldPt(point);
//====== Если
был захват, то перерисовываем
//====== контуры
двух соседних с узлом линий
if (m_bLock)
{
// Курсор должен
показывать операцию перемещения
SetCursor(m_hGrab);
//====== Установка
режима
pDC->SetROP2(R2_XORPEN);
//====== Двойное
рисование
//====== Сначала
стираем старые линии
RedrawLines(pDC,
pDoc->MapToLogPt (pDoc->
m_Poly.m_Points[ra_CurID]));
//====== Затем
рисуем новые
RedrawLines(pDC,
point);
//====== Запоминаем
новое положение вершины
pDoc->m_Poly.m_Points[m_CurID] = pt;
}
//====== Обычный
режим поиска близости к вершине
else
{
m_CurID = pDoc->FindPoint(pt);
// Если близко, то m_CurID получит индекс вершины
// Если далеко, то индекс будет равен -1
m_bReady = m_CurID
>= 0;
//=== Если близко, то меняем курсор
if (m_bReady)
SetCursor(m_hGrab);
}
}
//====== Перерисовка
двух линий, соединяющих
//====== перемещаемую
вершину с двумя соседними
void CDrawView::RedrawLines (CDC *pDC, CPointS point)
{
CTreeDoc *pDoc
= GetDocument();
//====== Ссылка
на массив точек текущего полигона
VECPTS& pts
= pDoc->m_Poly.m_Points;
UINT size = pts.sizeO;
//====== Если полигон вырожден, уходим
if (size
< 2) return;
//====== Индексы
соседних вершин
int il = m_CurID == 0 ? size - 1 : m_CurID - 1;
int 12
= m_CurID == size - 1 ? 0 : m_CurID + 1;
// ======
Берем перо и рисуем две линии
pDC->SelectObject(Sm_penLine);
pDC->MoveTo(pDoc->MapToLogPt(pts[11] ) ) ;
pDC->LineTo(point);
pDC->LineTo(pDoc->MapToLogPt(pts[12]));
}
Определение индекса вершины, к которой достаточно близко подобрался указатель мыши, производится в методе FindPoint класса документа. В случае если степень близости недостаточна, функция возвращает значение -1. Вставьте этот метод в файл реализации класса (TreeDoc.cpp):
int CTreeDoc::FindPoint(CDPointS pt)
{
//====== Пессимистический
прогноз
int id
= -1;
//====== Поиск
среди точек дежуоного полигона
for (UINT 1=0; i<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. Для этого:
Рис.
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-координаты вершин полигона?
Текущее состояние
команды меню или кнопки на панели инструментов легко определяется по их облику:
недоступная команда или кнопка имеет блеклый (grayed) вид. Кроме того, бывают
команды, работающие по принципу переключателя (включен — выключен). Их состояние
определяется по наличию — отсутствию флажка слева от команды меню или утопленному
— нормальному облику кнопки.
Отслеживание
состояния команд производится каркасом приложения в паузах между обработкой
сообщений. В эти моменты вызывается виртуальная функция Onldle, и если вы ее
переопределите для выполнения какой-либо своей фоновой задачи, то можете нарушить
или замедлить процесс отслеживания состояния команд. Логика перехода между состояниями
определяется специальными функциями т- обработчиками событий UPDATE_COMMAND_UI.
Мы должны создать такой обработчик для отслеживания состояния команды ID_EDIT_NEWPOLY.
Схема создания точно такая же, как и для самой команды, за исключением того,
что вы вместо строки COMMAND выбираете строку UPDATE_COMMAND_UI:
void CDrawView::OnUpdateEditNewpoly(CCradUI *pCmdUI)
{
pCmdUI->SetCheck(m_bNewPoints);
}
Метод SetCheck
вспомогательного класса ccmdui устанавливает флажок рядом с командой меню, если
параметр имеет значение TRUE, или снимает его, если параметр имеет значение
FALSE. Состояние кнопки на инструментальной панели синхронизировано с состоянием
команды меню, имеющей тот же идентификатор.
Следующим
шагом в развитии приложения будет введение в действие второй панели инструментов
IDR_Draw_TYPE. Загрузка из ресурсов панели инструментов осуществляется методом
LoadToolBar класса CToolBar. Так как объект этого класса (m_wndToolBar) хранится
в классе главного окна (CMainFrame), то и смену панелей инструментов целесообразно
выполнять в этом же классе. Введите в него новый метод:
void CMainFrame::ChangeToolbar(UINT tb)
{
//=== в параметре
tb будет передан идентификатор панели
m_wndToolBar.LoadToolBar
(tb) ;
//=== Перерисовка
toolbar
RecalcLayout();
}
Метод CFrameWnd::RecalcLayout
занимается перерисовкой панели инструментов и пересчетом размеров клиентской
области окна, так как панель инструментов хоть и управляется классом главного
окна, но расположена в клиентской области окна, отнимая у нее часть полезной
площади.
Имея в классе главного окна такую функцию, как ChangeToolbar, мы просто должны вызывать ее в нужные моменты, подавая на вход идентификатор той или иной панели. Осталось правильно определить те моменты, когда надо производить смену панелей. Очевидно, это моменты перевода фокуса из окна типа CTreeFrame в окно типа CDrawFrame и наоборот.
Здесь
важно понять, что фокус на самом деле попадает в одно из дочерних окон CLeftView
или CRightView или CDrawView. Но это происходит после того, как он попадет в
родительское окно-рамку. В принципе, возможны и другие варианты решения проблемы
своевременной смены панелей инструментов. Например, переопределить в каждом
из трех представлений виртуальную функцию OnActivateView и в ней вызывать ChangeToolbar.
Заметьте, что фокус может быть переведен в окно четырьмя разными способами:
Во всех четырех
случаях окну-рамке будет послано сообщение 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 в ту же папку, которая выбрана в левом окне.
Приложения, даже если они на первый взгляд функционируют корректно, надо тщательно тестировать, с тем чтобы проверить максимальное число состояний и ситуаций, в которых оно может оказаться. Так, тестируя настоящее приложение, я обнаружил два дефекта (не удивлюсь, если вы найдете еще больше). Первый состоит в том, что, если до выполнения команды Refresh изменить вручную позицию полос прокрутки, после выполнения команды происходит рассинхронизация полос. Лекарство оказалось простым — вставить в нужное место строку с вызовом:
ScrollToPosition(CPoint(0,0));
Эта функция является методом класса CScrollview, она устанавливает обе полосы прокрутки в исходные состояния. Определение места вставки мы оставляем читателю.
Кроме того,
логика приложения нарушается, если окна мини-чертежей (cwndGeom) видны не полностью,
а только частично. Курсор в этом случае не изменяет свою форму, несмотря на
то что он выходит за границы окна CRightView, то есть за границы клиентской
области окна CTreeFrame. Этот эффект наблюдается только при выходе в сторону
гипотетического продолжения окна cwndGeom. Объяснение в том, что мы захватили
мышиные сообщения (SetCapture) и направляем их в частично скрытое окно типа
CWndGeom, которое не отпускает мышь, так как справедливо считает, что курсор
находится над его прямоугольником. Окно не знает, что та его часть, которая
находится под курсором, в данный момент скрыта окном-рамкой или полосами прокрутки.
Вы помните, что полосы прокрутки являются частью клиентской области окна? Если
диагноз поставлен точно, то и лечение будет эффективным. Ниже приведена новая
версия обработки сообщения
WM_MOUSEMOVE
В Классе CWndGeom:
void CWndGeom::OnMouseMove(UINT nFlags, CPoint point)
{
//====== Два
прямоугольника (CWndGeom и CRightView)
CRect rChild,
rParent;
//=== Определяем экранные координаты (не клиентские!)
GetWindowRect(rChild) ;
m_pView->GetWindowRect
(rParent) ;
//=== Если есть
полосы прокрутки, то уменьшаем
//=== прямоугольник
окна на толщину полос
if (m_pView->m_szScroll.cx
- m_j>View->m_szView.cx > 0)
rParent . right -= SM_CXHSCROLL;
if (m_pView->m_szScroll.cy
- m_pView->m_szView.cy > 0)
rParent. bottom
-= SM_CYVSCROLL ;
//=== Ищем пересечение прямоугольников, обрезая rChild
rChild.IntersectRect (rChild, rParent);
//=== Приводим к экранным координаты указателя мыши
ClientToScreen
(Spoint) ;
//=== Если мышь попала в усеченный прямоугольник,
if ( rChild.
PtlnRect (point))
{
//=== то демонстрируем
активное состояние,
// изображая
рамку внутри прямоугольника CWndGeom
if (GetCaptureO
!= this)
{
SetCapture()
;
//=== Координаты относительные (клиентские)
CRect r (mJRect) ;
r.DeflateRect (4, 4);
CClientDC do
(this) ;
//====== Обрамляем
выбранный рисунок
dc.FrameRect
(Sr, SCBrush (RGB (192, 192, 255) ) ) ;
}
else
{
//=== Это происходит один раз при выходе из окна
ReleaseCapture () ;
Invalidate () ;
}
}
Здесь я решил
применить другой способ обрамления — с помощью функции FrameRect. Она хороша
тем, что не закрашивает внутренность прямоугольника, но обладает тем недостатком,
что рамка не может иметь толщину более одной логической единицы. Приведем еще
один вариант обрамления, использующий толстое перо и прозрачную кисть. Здесь
приведен только тот фрагмент, в котором произошли изменения:
if (rChild. PtlnRect (point) )
{
if (GetCaptureO != this)
{
SetCapture ()
;
CPen pen (PS_SOLID,
4, RGB (192, 192, 255) );
CClientDC dc(this)
;
dc. SelectObject
(&pen) ;
CRect r (m_Rect)
;
//====== Уменьшаем
прямоугольник
r .DeflateRect
(4,4) ;
//=== Выбираем прозрачную кисть для того, чтобы
//=== не закрасить его содержимое
dc. SelectObject (GetStockObject (NULL_BRUSH) ) ;
dc. Rectangle (r) ;
}
}
На рис. 5.4
приведен вид приложения в момент, когда курсор мыши расположен над окном, отображающим
данные неактивного документа. Рамка и курсор обозначают состояние готовности
к произведению выбора.
Рис. 5.4. Три представления одного документа
В предыдущем
разделе мы научились редактировать данные документа, воздействуя мышью непосредственно
на их представление, то есть облик документа, на экране монитора. Это довольно
грубый, но быстрый и эффективный способ, позволяющий получить заготовку некоторой
геометрии конструкции, которую впоследствии можно довести до желаемого состояния
с помощью таблиц (элементов управления типа 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.
Для управления
диалогом необходимо создать новый класс. Для этого можно воспользоваться контекстным
меню, вызванным над формой диалога.
Просмотрите объявление класса 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 в классе нестандартного окна редактирования.
Найдите заготовку
тела функции 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 создать переменные.
Это удобно делать с помощью мастера Studio.Net Add Member Variable.
В результате
работы мастера в классе CPolyDlg должна появиться новая переменная m_nPen типа
UINT, которая будет хранить толщину пера текущего полигона и обмениваться числовым
значением с полем IDC_PEN диалога. Обмен происходит при вызове функции:
DDX_Text(pDX,
IDC_PEN, m_nPen);
который происходит
в теле функции DoDataExchange. Указанная строка программы была автоматически
вставлена мастером Member Variable wizard.
Здесь
я вынужден просить прощения у читателя за неполную информацию, так как моя бета-версия
Studio.Net не позволяет автоматизировать процесс создания переменных и связывания
их с элементами управления диалога в той степени, в которой это должно быть.
Я уверен, что в той версии, с которой будете иметь дело вы, этот процесс будет
более эффективным. А сейчас вынужден закрывать и вновь открывать мастер для
каждой новой переменной. Мне ничего не остается, кроме как посоветовать повторить
вышеописанный процесс для создания еще трех переменных (m_nRed, m_nGreen, m_nBlue)
того же типа UINT, но связанных с другими окнами.
Для синхронизации
положений ползунков со значениями в окнах редактирования необходимо создать
еще четыре переменные (m_cColor, m_cRed, m_cGreen и m_cBiue), связанные с теми
же окнами IDC_COLOR, IDC_RED, IDC_GREEN и IDC_BLUE. На сей раз переключатель
Control-Value должен быть установлен в положение Control, а в поле Variable
Type должен быть указан или выбран наш новый класс
CClrEdit. Используя ту же технику, создайте три переменные (m_rSlider, m_gSlider
и m_bSlider) типа Control и свяжите их с тремя ползунками. При этом в поле Variable
Type: должен быть выбран класс CSliderCtrl.
Ограничения
на числовые значения, вводимые пользователем в окна редактирования, реализуются
с помощью функций динамической проверки данных. Это функции типа DDV_ (Dynamic
Data Validation), которые, так же как и функции DDX_, создаются с помощью мастера
Member Variable Wizard. Однако эту часть работы в бета-версии Studio.Net приходится
делать вручную. Вам придется самостоятельно найти справку по использованию мастера
для автоматизации создания функций проверки данных. Тема в указателе справочной
системы обозначена как Dialog Data Exchange and Validation. Важной особенностью
использования функций типа DDV_ является то, что для каждого элемента управления
вызовы DDV_ -функций должны непосредственно следовать за вызовами оох_-функций.
Нам надо задать ограничения на значения цвета (0-255) и значение толщины пера
(0-100). В конечном счете функция DoDataExchange должна приобрести вид:
void CPolyDlg::DoDataExchange(CDataExchange* pDX)
{
//====== Связывание
Control-переменных с ползунками
DDX_Control(pDX,
IDC_BSLIDER, m_bSlider);
DDX_Control(pDX,
IDCJ3SLIDER, m_gSlider);
DDX_Control(pDX,
IDC_RSLIDER, m_rSlider);
//==== Связывание
Control-переменных с нестандартными
//==== окнами
редактирования
DDX_Control(pDX,
IDC_COLOR, m_cColor) ;
DDX_Control(pDX,
IDC_BLUE, m_cBlue);
DDX_Control(pDX,
IDC_GREEN, m_cGreen);
DDX_Control (pDX,
IDC_RED, m_cRed) ;
//==== Связывание Value-переменных с нестандартными
//==== окнами редактирования и проверка данных
DDX_Text(pDX, IDC_BLUE, m_nBlue);
DDV_MinMaxUInt(pDX, m_nBlue, 0, 255);
DDX_Text (pDX, IDC_GREEN, m_nGreen);
DDV_MinMaxUInt(pDX, m_nGreen, 0, 255);
DDX_Text(pDX, IDC_RED, m_nRed) ;
DDV_MinMaxUInt(pDX, m_nRed, 0, 255);
DDX_Text(pDX, IDC_PEN, m_nPen);
DDV_MinMaxUInt(pDX,
m_nPen, 1, 100);
//==== Вызов родительской версии функции обмена CDialog::DoDataExchange(pDX);
}
Обработка сообщений от элементов управления
В окно диалога
мы ввели четыре кнопки, при нажатии которых в класс диалогового окна посылается
уведомляющее сообщение BN_CLICKED. При изменении данных в окнах редактирования
посылаются другие сообщения EN_CHANGE. При воздействии на ползунки также посылаются
уведомляющие сообщения, которые мы рассматривали в предыдущей главе. Однако,
как было отмечено, ползунки посылают и обычные сообщения (WM_HSCROLL или WM_VSCROLL).
Если в окне диалога имеется более одного ползунка, то сообщения от них удобно
обработать в одной функции, которая вызывается в ответ на сообщение о прокрутке.
Введите в класс CPolyDlg реакцию на WM_HSCROLL, так как наши ползунки ориентированы
горизонтально:
void CPolyDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
//====== Неинтересное
для нас сообщение
if (nSBCode==SB_ENDSCROLL)
return;
//====== Устанавливаем
флаг сообщений от ползунков
m_bScroll = true;
//====== Узнаем
идентификатор активного ползунка
switch(GetFocus()->GetDlgCtrlID())
{
case IDC_RSLIDER:
//====== Считываем
текущую позицию движка
m_nRed = m_rSlider.GetPos();
//====== Синхронизируем
поле, редактирования
SetDlgltemlnt(IDC_RED,
m_nRed);
break;
case IDC_GSLIDER:
m_nGreen = m_gSlider.GetPos();
SetDlgltemlnt(IDC_GREEN,
m_nGreen);
break; case
IDC_BSLIDER:
m_nBlue = m_bSlider.GetPos()
;
SetDlgltemlnt(IDC_BLUE,
m_nBlue);
break;
}
//====== Снимаем
флаг сообщений от ползунков
m_bScroll = false;
}
Сообщения
от всех ползунков обрабатываются в одной функции. Идентификатор активного ползунка
определяется путем последовательного вызова двух функций GetFocus и GetDlgctrliD,
являющихся методами класса cwnd.
Флаг сообщений
от ползунков (m_bScroll) понадобился нам для того, чтобы при синхронизации элементов
управления не происходили повторные вызовы функций-обработчиков. Дело в том,
что при изменении позиции ползунка мы должны привести в соответствие окно редактирования,
а при ручном изменении числа в окне редактирования мы должны синхронизировать
позицию ползунка. Но сообщение EN_CHANGE посылается как при ручном изменении,
так и при программном изменении с помощью функции SetDlgltemlnt. Проследим цепь
таких событий: пользователь подвинул движок ползунка, мы вызываем SetDlgltemlnt,
она провоцирует посылку сообщения EN_CHANGE, а обработчик этого сообщения корректирует
положение ползунка, которое и без того верно.
Введите в
класс диалога реакции на уведомления EN_CHANGE от четырех элементов IDC_PEN,
IDC_RED, IDC_GREEN И IDC_BLUE. Вы помните, что это надо делать с помощью кнопки
Events в окне Properties. Вставьте коды в остовы функций обработки, как показано
ниже:
void CPolyDlg::OnChangePen(void)
{
BOOL bSuccess;
//====== Попытка преобразовать в число
UINT nSize =
GetDlgltemlnt(IDC_PEN, SbSuccess, FALSE);
if (bSuccess
&& nSize < 101)
{
m_nPen = nSize;
m_pDoc->m_Poly-m_nPenWidth
= m_nPen;
m_pDoc->UpdateDrawView();
}
}
Отметьте,
что здесь мы намеренно не пользуемся функцией UpdateData, которая провоцирует
обмен данными сразу со всеми полями окна диалога, так как хотим показать более
экономный способ выборочного (целевого) обмена с помощью функции GetDlgltemlnt.
Правда, при таком подходе не работают функции проверки данных типа DDV_ и приходится
производить проверку самостоятельно:
void
CPolyDlg::OnChangeRed(void)
{
//====== Если
сообщение спровоцировано ползунком,
//====== то обходим
коды его синхронизации
if (!m_bScroll)
{
m_nRed = GetDlgltemlnt(IDC_RED,
0, FALSE);
m_rSlider.SetPos(m_nRed);
//====== Изменяем
цвет фона окна редактирования
m_cRed.ChangeColor(RGB(m_nRed, 0, 0));
//====== Корректируем
интегральный цвет
UpdateColor();
}
void CPolyDlg::OnChangeGreen(void)
{
if (!m_bScroll)
{
m_nGreen = GetDlgltemlnt(IDC_GREEN,
0, FALSE), m gSlider.SetPos(m_nGreen);
m_cGreen.ChangeColor(RGB(0, m_nGreen, 0)); UpdateColor ();
}
void CPolyDlg::OnChangeBlue(void)
{
if (!m_bScroll)
{
m_nBlue = GetDlglteralnt(IDC_BLUE, 0, FALSE);
m_bSlider.SetPos(m_nBlue);
}
m_cBlue.ChangeColor(RGB(0, 0, m_nBlue));
UpdateColor ();
}
Введите тело
вспомогательной функции, которая вычисляет интегральный цвет и вносит изменения,
перекрашивая окно диалога IDC_COLOR, и с помощью документа текущий полигон в
окне CDrawView:
void CPolyDlg::UpdateColor()
{
COLORREF clr
= RGB (m_riRed,m_nGreen,m_nBlue) ;
m_cColor.ChangeColor(clr)
;
m_pDoc->m_Poly.m_BrushColor
= clr;
m_pDoc->UpdateDrawView();
}
С помощью
Studio.Net введите в класс диалога реакции на уведомляющие сообщения (BN_CLICKED)
о нажатии кнопок выбора стандартных геометрий для полигонов (IDCJTRI, IDC_PENT
и IDC_STAR). В них мы с помощью техники обратного указателя вновь обращаемся
к документу и используем его данные и методы для замены координат точек текущего
полигона:
void CPolyDlg::OnClickedTri(void)
{
m_pDoc->m_Poly.MakeTria()
;
m_pDoc->UpdateDrawView() ;
}
void CPolyDlg::OnClickedPent(void)
{
m_pDoc->m_Poly.MakePent()
;
m_pDoc->UpdateDrawView() ;
}
void CPolyDlg::OnClickedStar(void)
{
m_pDoc->m_Poly.MakeStar()
;
m_pDoc->UpdateDrawView();
}
Измените тело
конструктора диалогового класса, с тем чтобы при открытии диалога он смог запомнить
обратный указатель (адрес документа) и все его элементы были правильно инициализированы:
CPolyDlg::CPolyDlg(CTreeDoc*
p)
: CDialog (CPolyDlg::IDD, 0)
{
m_pDoc = p;
m_nPen = p->m_Poly.m_nPenWidth;
//====== Расщепляем
цвет фона текущего полигона
COLORREF brush
= p->m_Poly.m_BrushColor;
m_nRed = GetRValue(brush);
// на три компонента
m_nGreen = GetGValue(brush);
m_nBlue = GetBValue(brush)
;
m_bScroll = false;
// Ползунки в покое
Кнопкам управления,
которые обычно размещаются в окне диалога, тоже можно придать нестандартный
облик, пометив их bitmap-изображениями вместо традиционного текста. Для этой
цели в библиотеке MFC имеется специальный класс CBitmapButton, объекту которого
можно приписать до четырех изображений, соответствующих различным состояниям
кнопки. Кнопка может быть в одном из следующих состояний:
Достаточно
создать одно изображение кнопки, соответствующее первому состоянию, чтобы она
функционировала. Размеры bitmap-изображений могут быть любыми, но важно, чтобы
они были одинаковы. Система задает такой размер кнопке, какой имеет ее изображение
в нормальном (первом) состоянии. При создании bitmap-ресурсов им следует придать
идентификаторы в соответствии со следующими правилами:
Заметьте,
что выбор изображения происходит, опираясь на заголовок кнопки, а не на его
идентификатор. Применим эту технологию для трех наших кнопок с заголовками TRI,
PENT и STAR и придадим им нестандартный облик. Для этого:
BOOL CPolyDlg::OnInitDialog()
{
//====== Загрузка
из ресурсов изображений кнопок
m_cTri.AutoLoad
(IDCJTRI, this);
m_cPent.Autoload
(IDC_PENT, this);
m_cStar.AutoLoad
(IDC_STAR, this);
CDialog::OnlnitDialog{);
//====== Установка
диапазона ползунков
m_rSlider.SetRange (0, 255);
m_gSlider.SetRange (0, 255);
m_bSlider.SetRange
(0, 255);
//====== Установка
цены деления ползунков
m_rSlider.SetTicFreq (50);
m_gSlider.SetTicFreq (50);
m_bSlider.SetTicFreq
(50);
//=== Вызов обработчиков для начальной
//=== закраски окон и установки ползунков OnChangeRedO ;
OnChangeGreen();
OnChangeBlue ();
return TRUE;
}
В Visual Studio
6 эта функция создавалась как обработчик сообщения WM_INITDIALOG, здесь в Studio.Net
7.0 я не обнаружил сообщения с таким именем в списке сообщений диалогового класса.
Однако в списке Overrides присутствует строка с именем OnlnitDialog. В принципе
рассматриваемая функция и в Visual Studio 6 имеет прототип virtual BOOL OnlnitDialog,
но classWizard 6-й версии причисляет ее к функциям-обработчикам сообщений. Характерным
моментом является также то, что прототип функции в Studio.Net изменился и стал
BOOL OnlnitDialog (void);. Возвращаясь к диалоговому классу, заметим, что обращение
к методам класса CTreeDoc требует включить традиционную строку
#include "TreeDoc.h"
в список директив препроцессора файла PolyDlg.cpp.
Особенность работы с немодальным диалогом заключается в том, что надо затратить дополнительные усилия для корректного завершения его работы. Чтобы закрыть немодальный диалог, документация требует переопределить две виртуальные функции в классе диалога: обработчик закрытия диалога OnCancel и функцию PostNcDestroy. Существуют подробные рекомендации для завершения немодального диалога. Внутри вашей версии виртуальной функции OnCancel следует уничтожить окно диалога (DestroyWindow), а внутри другой виртуальной функции PostNcDestroy рекомендуется уничтожать объект диалогового класса (delete this;). С немодальным диалогом принято работать динамически (on heap frame), а не статически (on stack frame), как с модальным. Когда уничтожается окно Windows, то последним сообщением, которое ему посылается, является WM_NCDESTROY. Обработчик по умолчанию, то есть родительская версия cwnd: :OnNcDestroy, открепляет (detach) описатель окна HWND от объекта класса C++ и вызывает виртуальную функцию PostNcDestroy.
Некоторые классы переопределяют ее для того, чтобы произвести освобождение своих объектов в области heap (динамическая память). При неудаче в процессе создания окна происходит вызов функции cwnd::PostNcDestroy, которая ничего не делает, но дает возможность виртуальным двойникам корректно освободить динамическую память. Мы тоже будем работать с диалогом динамически (on heap frame). В классе документа будет храниться адрес объекта с Pol у Dig, который должен корректно следить за стадиями создания и уничтожения диалога. Я намеренно не переопределял PostNCDestroy, чтобы показать альтернативный способ, допустимый в частных случаях. Так как наш диалог завершается командой Close с идентификатором IDOK, то, введя обработчик этой команды, мы можем с помощью родительской версии уничтожить Windows-окно, а затем освободить память, занимаемую объектом собственного класса:
void CPolyDlg::OnClickedOk(void)
{
//=== Запоминаем
факт отсутствия диалога в документе
m_pDoc->m_pPolyDlg
= 0;
//====== Родительская
версия вызовет DestroyWindow
CDialog::OnOK();
//====== Мы освобождаем
память
delete this;
}
Вызов диалога
производится в ответ на команду Edit > Poly Color, которая уже присутствует
в меню IDR_DrawTYPE. Введите в класс CTreeDoc обработчик этой команды и наполните
его кодами, как показано ниже:
void CTreeDoc::OnEditPolycolor(void)
{
//====== Если
диалог отсутствует
if (!m_pPolyDlg)
{
//====== Создаем
его в две ступени
m_pPolyDlg =
new CPolyDlg(this);
m_pPolyDlg->Create(IDD_POLYCOLOR) ;
}
else
//===== Иначе
делаем активным его окно
m_pPolyDlg->SetActiveWindow();
}
Здесь использован
указатель на объект диалогового класса, который необходимо ввести в число public-данных
класса CTreeDoc (CPolyDlg *m_pPolyDlg;) и обнулить в конструкторе документа
(m_pPolyDlg = 0;). Сделайте это, а также введите в файл реализации класса CTreeDoc
вспомогательную функцию UpdateDrawView:
void CTreeDoc::UpdateDrawView()
{
//====== Добываем
адрес нужного представления
CDrawView *pView
= dynamic_cast<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).