Визуальное
редактирование данных
Несмотря на
то что разрабатываемое приложение носит учебный характер, оно моделирует вполне
реальные ситуации, когда путем навигации по дереву файлов пользователь ищет
и выбирает документ, для того чтобы открыть его в окне представления, специализированного
для внесения изменений в данные. В отличие от
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. Ситуацию повторного рисования можно
представить так:
- цвет каждого пиксела
(каждой точки растра) при рисовании определяется путем суммирования цвета
фона и цвета пера по законам операции XOR;
- если перо красное (8
младших бит цвета установлены в 1), а фон белый (то есть присутствуют все
3 компонента цвета — 3 байта установлены в 1), то результатом операции XOR
будет цвет Cyan, так как красный компонент исчезнет (1+1=0). Оставшиеся
же компоненты, зеленый и синий, дают цвет Cyan;
- если еще раз пройтись
красной линией по тому же месту (по линии цвета Cyan), то при сложении
цветов единицы попадут на нули и цвет будет белый (все 3 байта станут равны
1).
Итак, повторный
проход стирает линию. В качестве упражнения повторите выкладки при условии,
что перо белое (затем — черное). Такие упражнения шлифуют самое главное качество
программиста — упорство. При черном пере вы должны получить что-то не то. Тем
не менее мы берем черное перо, но при этом задаем стиль PS_DOT, что в принципе
равносильно черно-белому перу. Белые участки работают как описано, а черные
своей инертностью помогают создать довольно интересный эффект переливания пунктира
или эффект натягивания и сжимания резинки. Есть еще одно значение (К2_ыот) параметра
функции SetROP2, которое работает успешно, но не без эффекта резинки.
Примечание
Я думаю, что цифра 2 в
имени функции означает намек на фонетическую близость английских слов «two»
и «to». Если предположение верно, то имя функции SetROP2 можно прочесть как
«Set Raster Operation To», что имеет смысл установки режима растровой операции
в положение (значение), заданное параметром функции. Обязательно просмотрите
справку по этой функции (методу класса CDC), для того чтобы узнать ваши возможности
при выборе конкретного режима рисования.
Режим перетаскивания
вершин полигона готов к использованию в момент вхождения указателя мыши в область
чувствительности вершины (за этим следит флаг m_bReady). Кроме данного режима
мы реализуем еще один режим — режим создания нового полигона (флаг m_bNewPoints),
который вступает в действие при выборе команды меню Edit > New Poly. При
анализе кода обратите внимание на то, что мы получаем от системы координаты
точек в аппаратной системе, а запоминать в контейнере точек должны мировые (World)
координаты. Преобразование координат осуществляется в два этапа:
- сначала из Device-пространства
в пространство Page (функция DPtoLP — Device Point to Logical Point);
- затем из Page-пространства
в пространство World (наша функция MapToWorldPt).
Теперь вы,
вероятно, подготовлены к восприятию того, что происходит в следующих трех методах
класса 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. Для этого:
- Поставьте фокус на элемент
CDrawView в представлении классов (Class View) и перейдите в окно Properties.
- Нажав кнопку Events,
выберите идентификатор ID_EDIT_NEWPOLY, раскройте маркер (+) и выберите COMMAND
(первую из двух выпавших строк).
- Создайте обработчик,
выбрав <Add> в выпадающем списке справа от COMMAND.
Рис. 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-координаты
вершин полигона?