Работа с Visual Studio.Net

         

Визуальное редактирование данных

Несмотря на то что разрабатываемое приложение носит учебный характер, оно моделирует вполне реальные ситуации, когда путем навигации по дереву файлов пользователь ищет и выбирает документ, для того чтобы открыть его в окне представления, специализированного для внесения изменений в данные. В отличие от

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. Для этого:

  1. Поставьте фокус на элемент CDrawView в представлении классов (Class View) и перейдите в окно Properties.
  2. Нажав кнопку Events, выберите идентификатор ID_EDIT_NEWPOLY, раскройте маркер (+) и выберите COMMAND (первую из двух выпавших строк).
  3. Создайте обработчик, выбрав <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-координаты вершин полигона?

Содержание раздела