Иллюстрированный самоучитель по 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. Для этого:

  • Поставьте фокус на элемент 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-координаты вершин полигона?




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