Форум программистов, компьютерный форум, киберфорум
Наши страницы
ashsvis
Войти
Регистрация
Восстановить пароль
"Орешек знаний тверд, Но все же мы не привыкли отступать..." (с)
Оценить эту запись

Шаг 6 (графический векторный редактор). Изменение формы фигуры путём перетаскивания узловых маркеров

Запись от ashsvis размещена 12.11.2018 в 19:42

Приветствую вас, уважаемые изучатели моих способов строительства простого графического векторного редактора.

В этой статье рассмотрим работу с маркерами изменения узлов фигуры (точек, по которым рисуется фигура) и об алгоритме
выбора узлов, добавления новых узлов и удалении существующих узлов. Это как раз тот материал, ради которого всё и
затевалось.

Когда мы переключаемся в режим изменения узлов, маркеры размеров скрываются и начинают рисоваться маркеры узлов.
Переключиться в режим изменения узлов можно только для одной фигуры. Если выделено несколько фигур, то элемент
контекстного меню на фигуре (по правой кнопке мышки) для включения режима изменения узлов не появится.

Всего четыре действия мы можем сделать через меню:
  1. Начать изменение узлов.
  2. Добавить узел.
  3. Удалить узел.
  4. Выйти из режима изменения узлов.

Для начала изменения узлов необходимо присвоить свойству NodeChaging значение True:
C#
1
2
3
4
5
6
7
8
9
10
11
12
/// <summary>
/// Режим выбора и перетаскивания узловых точек (изменения узлов)
/// </summary>
public bool NodeChanging
{
    get { return _nodeChanging; }
    set
    {
        _nodeChanging = value;
        _container.Invalidate();
    }
}
Свойство публичное, поэтому этот режим можно включить извне, из меню пользователя. При изменении свойства в
сеттере делается запрос на перерисовку контейнера, что и понятно - размерные маркеры заменяются на узловые.

Узловой маркер, также как и размерный, выбирается пользователем через метод клика мышки на контейнере
ContainerMouseDown(), далее метод MarkerIndex = PointInMarker(e.Location, out fig) должен вернуть отрицательный
индекс маркера, отрицательный - значит маркер изменения узла.

В методе обработки перемещения мышки ContainerMouseMove() рассчитывается величина смещения от точки первого нажатия
до текущего положения мышки (см. шаг 5 - метод расчёта смещения один, важен только режим NodeChanging).

В рисующем методе DrawFocusFigure() для маркера изменения узла написано следующее:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private static void DrawFocusFigure(Figure figure, Graphics graphics, PointF offset,
                                    int markerIndex, bool nodeChanging)
{
    ....
    else
        if (nodeChanging && (markerIndex < 0))
    {
        // тянут мышкой за маркер, изменяющий положение узла
        using (var gp = new GraphicsPath())
        {
            if (figure.Kind == DrawingKind.Polygon)
                gp.AddPolygon(figure.GetPoints());
            else
                gp.AddLines(figure.GetPoints());
            var ps = gp.PathPoints;
            var i = Math.Abs(markerIndex) - 1;
            if ((i >= 0) && (i < ps.Length))
            {
                ps[i].X += offset.X;
                ps[i].Y += offset.Y;
                DrawCustomFigure(graphics, ps, figure.Kind);
            }
        }
    }
    ....
}
При помощи методов класса GraphicsPath сначала мы рисуем все точки фигуры, потом в строке 15 получаем массив точек
пути, в строке 16 нормируем индекс узла (чтобы он был положительным числом и начинался с нуля), небольшая защита
в строке 17, индекс i должен быть в диапазоне массива, а элемент массива ps[i] - это и есть та узловая точка, которую
пользователь "тащит" мышкой. Поэтому смещаем только её. Метод DrawCustomFigure() рисует будущую фигуру.

Фиксация новой формы фигуры производится в методе ContainerMouseUp():
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
private void ContainerMouseUp(object sender, MouseEventArgs e)
{
       ....
                else if (NodeChanging && (MarkerIndex < 0) && _selected.Count == 1) // тянут за маркер узла
                {
                    var fig = _selected[0];
                    UpdateSize(fig, _mouseOffset, MarkerIndex);
                    _container.Invalidate();
                }
                break;
        }
        ....
}
В строке 6 получаем ссылку на одну единственную фигуру и вызываем для неё метод UpdateSize(), в котором:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void UpdateSize(Figure fig, PointF offset, int marker)
{
    PointF[] pts;
     ....
    if (NodeChanging && (marker < 0))
    {
        // перемещение узлов
        pts = fig.GetPoints();
        var index = Math.Abs(marker) - 1;
        if ((index >= 0) && (index < pts.Length))
        {
            pts[index].X += offset.X;
            pts[index].Y += offset.Y;
            fig.SetPoints(pts);
        }
    }
}
В общем тоже самое что и в методе для рисования DrawFocusFigure(), только в строке 14 мы возвращаем все точки,
вместе с одной изменённой обратно в массив точек фигуры методом fig.SetPoints(pts).

Про перемещение узловых точек я рассказал. Но можно также добавлять новые точки на сторонах фигуры, делая её сложнее.
Для этого имеется метод AddNode():
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// <summary>
/// Метод добавления нового узла на выбранный сегмент линии
/// </summary>
public void AddNode()
{
    if (_selected.Count != 1) return;
    var figure = _selected[0];
    var ps = figure.GetPoints();
    var pts = new PointF[ps.Length + 1];
    ps.CopyTo(pts, 0);
    // замыкание контура фигуры
    pts[pts.Length - 1].X = pts[0].X;
    pts[pts.Length - 1].Y = pts[0].Y;
    for (var i = 1; i < pts.Length; i++)
    {
        // поиск сегмента линии, куда бы можно добавить новый узел
        if (!PointInRange(figure, MouseDownLocation, pts[i - 1], pts[i])) continue;
        var points = new List<PointF>(figure.GetPoints());
        points.Insert(i, MouseDownLocation);
        figure.SetPoints(points.ToArray());
        break;
    }
    _container.Invalidate();
}
Новый узел мы можем добавить только на какой-нибудь стороне фигуры, то есть на отрезке между двумя вершинами (узлами).
Для этого в строке 17 мы вызываем метод PointInRange() последовательно для каждых двух соседних точек и проверяем
попадание координат мышки в область отрезка с некоторым допуском.

Таким образом, если попытаться добавить новый узел, кликнув на середине фигуры, то место для новой точки не будет
найдено. В строке 19 мы вставляем новую точку с координатами MouseDownLocation между индексами i-1 и i, а потом
"заливаем" все точки обратно в массив точек фигуры.

Удаление узловой точки (вершины) фигуры. Для удаления узла имеется соответствующий метод RemoveNode(markerIndex):
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// <summary>
/// Метод удаления выбранного маркера
/// </summary>
/// <param name="markerIndex">индекс маркера для удаления</param>
public void RemoveNode(int markerIndex = 0)
{
    if (_selected.Count != 1) return;
    if (markerIndex == 0)
        markerIndex = MarkerIndex;
    var figure = _selected[0];
    var ps = figure.GetPoints();
    if ((ps.Length <= (figure.Kind == DrawingKind.Polygon ? 3 : 2)) ||
        (markerIndex >= ps.Length - 1)) return;
    var points = new List<PointF>(figure.GetPoints());
    points.RemoveAt(Math.Abs(markerIndex) - 1);
    figure.SetPoints(points.ToArray());
    _container.Invalidate();
}
По индексу ищется элемент массива и удаляется. Перед удалением проверяется количество оставшихся точек фигуры.
Если фигура замкнутая, то минимум точек у неё - три, а если линия, то минимум - две точки.

Вот всё, что я хотел пояснить по поводу изменения узловых точек (вершин) фигуры.

В следующей статье мы будем, наконец, применять наши классы в деле.

Для этого я подготовил некую "оболочку":

Нажмите на изображение для увеличения
Название: Скриншот рабочей версии.jpg
Просмотров: 85
Размер:	132.8 Кб
ID:	5083

До скорого!
Всего комментариев 8
Комментарии
  1. Старый комментарий
    Аватар для Storm23
    ashsvis,
    1) Маркер должен быть отдельным объектом. Вот эти все индексы, отрицательные индексы - это все фтопку. Должен быть просто список маркеров. Индексы с непонятной семантикой - зачем??
    2) Удаление и добавление точек в фигуру - должны быть внутренними методами фигур. Ибо инкапсуляция. И вообще у вас очень много кода в интерфейсе. Это нехорошо.

    Ну и вообще все это по-другому нужно было бы делать. Маркеры должны давать сами фигуры. Потому что только они знают какие маркеры для них нужны, и как реагировать на передвижение маркеров.
    Например, квадрат можно тянуть только за правый нижний маркер, потому что квадрат нельзя растягивать по ширине или высоте. Круг например, нет смысла вращать, поэтому круг бы не возвращал маркер вращения (впрочем его у вас и так нет, да и круга тоже).
    Добавление и удаление точек тоже не имеет смысла, если это квадрат или окружность. И так далее.
    Смысл в том, что только сама фигура знает как должны работать маркеры для нее. Интерфейс же должен только получать список маркеров от фигуры и возвращать в фигуру передвижение маркеров.

    Да, и еще. Чисто методологически. Если вы пишите о коде, нужно выкладывать этот код. Я имею ввиду проект целиком, в котором можно поковыряться, посмотреть, потестить, подумать. Это же не лекция по абстрактным наукам. Здесь нужно пробовать руками. Иначе все это не имеет смысла.
    Запись от Storm23 размещена 12.11.2018 в 20:42 Storm23 вне форума
    Обновил(-а) Storm23 12.11.2018 в 20:46
  2. Старый комментарий
    Аватар для ashsvis
    Storm23,
    1. Согласен, типов маркеров должно быть больше и к этом я буду приходить, конечно.
    Цитата:
    много кода в интерфейсе
    Цель моих статей не только в том, чтобы показать другим, как решал проблему я, но и получить опыт общения от более
    опытных коллег. Приложение фактически строится наряду с написанием статей, несколько обгоняя их (дня на три).
    Конечно используются два - три проекта из "ящика стола". Поэтому код для выкладки ещё "сыроват" для выкладки.
    Постараюсь выложить в следующей статье.

    И ещё я таким образом хочу привести в порядок программный код по этой теме. На самом деле я получаю большой кайф
    от всего этого процесса в целом.
    Запись от ashsvis размещена 12.11.2018 в 21:13 ashsvis вне форума
  3. Старый комментарий
    Аватар для netBool
    ashsvis, выложил бы уже на гитхаб свой шедевр)
    Запись от netBool размещена 14.11.2018 в 16:33 netBool вне форума
  4. Старый комментарий
    Аватар для ashsvis
    netBool, "шедевр" в процессе разработки, а гитхаб зачем?, тут обновления и получите, если таковые будут.
    Запись от ashsvis размещена 14.11.2018 в 17:52 ashsvis вне форума
  5. Старый комментарий
    Аватар для Storm23
    Цитата:
    а гитхаб зачем
    Во-первых, можно читать исходник не качая весь проект, что удобно.
    Во-вторых, можно указать строку кода непосредственно ссылкой.
    Ну и в-третьих, можно выкатывать обновления. Как вам так и другим.
    Запись от Storm23 размещена 14.11.2018 в 21:06 Storm23 вне форума
  6. Старый комментарий
    Аватар для Usaga
    GITHUB удобен вам, ибо это как никак, VCS. И пользователям удобен, ибо можно Issues заводить с багами, а так же следить за развитием проекта.
    Запись от Usaga размещена 15.11.2018 в 06:38 Usaga вне форума
  7. Старый комментарий
    Аватар для ashsvis
    Спасибо за совет. Про гитхаб слышал, но никогда не пользовался. Буду двигаться в этом направлении...
    Запись от ashsvis размещена 15.11.2018 в 06:49 ashsvis вне форума
  8. Старый комментарий
    Аватар для ashsvis

    Выложено на github.com

    Версия редактора из этой категории выложена по адресу: https://github.com/ashsvis/SimpleVectorGraphicsEditor

    И далее поддерживаться не будет, ибо нарабатывается более крутая и правильная версия в теме:
    http://www.cyberforum.ru/windows-forms/thread2352217.html#post13063894
    Запись от ashsvis размещена 16.11.2018 в 19:30 ashsvis вне форума
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2019, vBulletin Solutions, Inc.
Рейтинг@Mail.ru