Несмотря на то что разрабатываемое приложение носит учебный характер, оно моделирует вполне реальные ситуации, когда путем навигации по дереву файлов пользователь ищет и выбирает документ, для того чтобы открыть его в окне представления, специализированного для внесения изменений в данные. В отличие от
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, которое работает успешно, но не без эффекта резинки.
Примечание 1
Примечание 1
Я думаю, что цифра 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. Для этого: