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

Шаг третий (графический векторный редактор). Тестовая расстановка фигур

Запись от ashsvis размещена 09.11.2018 в 18:45

Некоторые изменения я внёс в названия свойств классов Stroke и Fill (чисто косметические, благо это не продакшен).
В классе Figure удалил этот ужасно некрасивый метод OneBuff(), а метод PointInFigure, где он использовался, теперь выглядит
следующим образом (спасибо Storm23 за подсказку):
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
27
28
29
30
31
/// <summary>
/// Метод проверяет принадлежность точки фигуре
/// </summary>
/// <param name="point">проверяемая точка</param>
/// <returns>True - точка принадлежит фигуре</returns>
public virtual Boolean PointInFigure(PointF point)
{
    switch (Kind)
    {
        case DrawingKind.Polygon:
            using (var gp = new GraphicsPath())
            {
                gp.AddPolygon(GetPoints());
                return (gp.IsVisible(point));
            }
        case DrawingKind.Polyline:
            using (var gp = new GraphicsPath())
            {
                var ps = GetPoints();
                for (var i = 1; i < ps.Length; i++)
                {
                    gp.StartFigure();
                    gp.AddLine(ps[i - 1], ps[i]);
                    gp.CloseFigure();
                }
                using (var pen = new Pen(Color.Black, Stroke.Width * 5f))
                    return gp.IsOutlineVisible(point, pen);
            }
    }
    return false;
}
Далее упрощать классы Stroke, Fill и Figure пока не буду, главным образом из-за того, что при
записи в xml-файл, настройки для карандаша и кисти будут в отдельных ветках и это будет очень гармонично смотреться.

Да, и ещё я сделал класс Figure не абстрактным и не буду создавать от него потомков типа Polygon и Polyline,
а буду пользоваться свойством Kind. В этом есть своё удобство, в том плане, что можно легко переходить от полигона
к полилинии и обратно, меняя это свойство.

Но всё же переходим к написанию класса Picture (или PictureEditor ?), пусть это будет его рабочее название.

Что я хочу, чтобы делал этот класс?
  1. Размещение фигур
  2. Перемещение фигур
  3. Изменение размеров
  4. Изменение узлов
  5. и ещё много чего хочу, но не всё сразу...

Ну, первое - это хранение элементов Figure. Будет два списка, первый:
C#
1
2
// список созданных фигур
private readonly List<Figure> _figures = new List<Figure>();
В нём будем хранить все нарисованные фигуры. Порядок расположения фигур в этом списке определяет, как фигуры
будут накладываться друг на друга. Сначала рисуется первая фигура (с индексом 0), потом вторая, при этом последующие
фигуры могут частично или полностью закрывать ранее нарисованные.
А вот когда мы кликаем на них мышкой, то выбираться будут сначала фигуры, которые нарисованы позже всех.
В "умных" книжках это называется z-ордером.

Второй список будет хранить выбранные элементы. Это подмножество первого списка, но тип его мы возьмём другой,
более удобный в плане оповещения об изменениях в списке выбранных элементов:
C#
1
2
// список выбранных фигур
private readonly ObservableCollection<Figure> _selected = new ObservableCollection<Figure>();
Выбор элементов делается мышкой, кликая по ним; можно выбрать несколько, делая выбор рамкой (которую в первом шаге делали).
Можно выбирать фигуры или исключать из уже выбранных, кликая мышкой с удерживанием клавиши Ctrl на клавиатуре.
Во всех этих случаях список выбора _selected будет изменяться, а при его изменении мы будем сбрасывать режим работы
редактора в обычный - для выбора фигур и изменения их размеров.

Специальный режим работы редактора - изменение положение узлов одной выбранной фигуры.
Режим работы с маркерами: "обычный" и "режим изменения узлов", и переключается при помощи свойства:
C#
1
2
3
4
/// <summary>
/// Режим выбора и перетаскивания узловых точек (изменения узлов)
/// </summary>
public bool NodeChanging { get; set; }
В режиме изменения узлов можно перетаскивать угловые точки у прямоугольника или концы простых отрезков линий,
а также добавлять новые узловые точки, превращая четырёхугольник в пятиугольник или простой отрезок линии в
ломанную, состоящую из двух сегментов. Узлы также можно удалять и оставлять не менее двух для линий и не менее
трёх - для полигонов.

С точки зрения перетаскивания фигур "мышкой" или изменения их размеров, создано свойство EditorMode:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
/// <summary>
/// Режим работы редактора
/// </summary>
public EditorMode EditorMode
{
    get { return _editorMode; }
    set
    {
        _editorMode = value;
        // выбор рамкой запрещён во всех режимах, кроме EditorMode.Selection
        _ribbonSelector.Disabled = _editorMode != EditorMode.Selection;
    }
}
Тип свойства EditorMode - это перечисление и объявлено как:
C#
1
2
3
4
5
6
7
8
/// <summary>
/// Режимы редактора
/// </summary>
public enum EditorMode
{
    Selection,
    Dragging
}
Когда режим редактора установлен в EditorMode.Selection, то мы выбираем фигуры или группы фигур. Когда режим редактора EditorMode.Dragging - мы тащим либо фигуры
(или группы фигур) целиком или изменяем их размер (при помощи маркеров).

1. Размещение фигур

Для текущей стадии разработки редактора необходимо уже размещать фигуру и тренироваться на ней. Поэтому, чтобы
особо не заморачивать вам голову (повторюсь: только на этой стадии тестирования!) добавлять фигуры будем прямо из
кода этого редактора, используя контекстное меню, которое будем создавать "на лету".

В обработчике нажатия правой кнопки "мышки" запишем:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <summary>
/// Нажатие кнопки "мышки" на PaintBox
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ContainerMouseDown(object sender, MouseEventArgs e)
{
    ...
   
    // если нажата правая кнопка мышки
    if (e.Button != MouseButtons.Right) return;
    // переключаем режим на выбор рамкой
    EditorMode = EditorMode.Selection;
    ShowContextMenu(e.Location);
 
    ... 
}
В методе ShowContextMenu() создаются пункты контекстного меню для создания тестовых фигур:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/// <summary>
/// Вызов контекстного меню
/// </summary>
/// <param name="location">точка вызова контекстного меню</param>
private void ShowContextMenu(Point location)
{
    _contextMenuStrip.Items.Clear();
    Figure fig;
    PointInMarker(location, out fig);
    // ищем фигуру в точке нажатия
    if (fig == null)
        fig = PointInFigure(location);
    // есть ли фигура под курсором мышки?
    if (fig == null) // это не фигура, показываем общее меню
    {
        // создаём пункт меню
        var miRectangle = new ToolStripMenuItem("Добавить фигуру");
        // подключаем обработчик
        miRectangle.Click += (o, args) =>
            {
                var rect = RectangleF.FromLTRB(location.X, location.Y,location.X + 50, location.Y + 50);
                var points = new List<PointF>
                    {
                        new PointF(rect.Left, rect.Top),
                        new PointF(rect.Left + rect.Width, rect.Top),
                        new PointF(rect.Left + rect.Width, rect.Top + rect.Height),
                        new PointF(rect.Left, rect.Top + rect.Height)
                    };
                fig = new Figure(DrawingKind.Polygon, points);
                _figures.Add(fig);
                _selected.Add(fig);
                _container.Invalidate();
            };
        // и добавляем его к меню
        _contextMenuStrip.Items.Add(miRectangle);
        // создаём пункт меню
        var miLine = new ToolStripMenuItem("Добавить линию");
        // подключаем обработчик
        miLine.Click += (o, args) =>
        {
            var rect = RectangleF.FromLTRB(location.X, location.Y, location.X + 50, location.Y + 50);
            var points = new List<PointF>
                    {
                        new PointF(rect.Left, rect.Top),
                        new PointF(rect.Left + rect.Width, rect.Top + rect.Height)
                    };
            fig = new Figure(DrawingKind.Polyline, points);
            _figures.Add(fig);
            _selected.Add(fig);
            _container.Invalidate();
        };
        // и добавляем его к меню
        _contextMenuStrip.Items.Add(miLine);
    }
 
    ...
 
    // показываем контекстное меню
    _contextMenuStrip.Show(_container, location);
}
В этом методе, если пользователь нажимает правую кнопку "мышки" вне созданных фигур, то контекстное меню
очищается (строка 7), потом проверяется, было ли нажатие на маркере (строка 9), если было, переменная fig будет
содержать ссылку на фигуру, у которой нажато на маркер. Если переменная fig пуста, то проверяется нажатие
на теле фигуры, в строках 11, 12. В строке 14 убеждаемся, что нажатие на фигурах не было и поэтому показываем
общее меню.

В строках с 17 по 35 создаём пункт меню для создания полигона, в строках с 37 по 54 создаём пункт меню для создания
полилинии. Обработчики, подключаемые здесь же, содержат код, создающий массивы из 4-х точек для полигона и из 2-х
точек для линии. Размер по умолчанию фигур - 50х50 пикселей, а положение - где пользователь клинул мышкой (это верхний
левый угол контекстного меню).

Потом создаётся объект Figure, с указанием типа фигуры и передаются ему в конструктор точки массива.
Созданная фигура добавляется в основной список _figures и сразу в список выбранных _selected.
В конце вызывается метод _container.Invalidate() с требованием о перерисовке поверхности контейнера.
То есть выполняется метод ContainerPaint, приведу его код целиком:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/// <summary>
/// Обработчик события рисования на поверхности контейнера
/// </summary>
/// <param name="sender">визуальный компонент с поверхностью для рисования</param>
/// <param name="e">объект параметров события со свойством Graphics</param>
private void ContainerPaint(object sender, PaintEventArgs e)
{
    // рисуем все созданные фигуры
    foreach (var fig in _figures) fig.DrawFigure(e.Graphics);
    if (EditorMode != EditorMode.Dragging)
    {
        if (NodeChanging)
        {
            // маркеры узлов рисуем круглыми
            foreach (var fig in _selected)
            {
                using (var gp = new GraphicsPath())
                {
                    var rects = GetNodeMarkers(fig);
                    foreach (var t in rects)
                        gp.AddEllipse(t);
                    e.Graphics.FillPath(Brushes.White, gp);
                    using (var pen = new Pen(Color.Black))
                    {
                        pen.Width = 0;
                        e.Graphics.DrawPath(pen, gp);
                    }
                }
            }
        }
        else
        {
            // рисуем маркеры размеров у выбранных фигур
            foreach (var fig in _selected)
            {
                using (var gp = new GraphicsPath())
                {
                    var sizeMarkers = GetBoundMarkers(fig.GetBounds);
                    foreach (var t in sizeMarkers.Where(t => t.Width > 2))
                        gp.AddRectangle(t);
                    e.Graphics.FillPath(Brushes.White, gp);
                    using (var pen = new Pen(Color.Black))
                    {
                        pen.Width = 0;
                        e.Graphics.DrawPath(pen, gp);
                    }
                }
            }
        }
    }
    // при перетаскивании
    if (EditorMode != EditorMode.Dragging) return;
    foreach (var fig in _selected)
        DrawFocusFigure(fig, e.Graphics, _mouseOffset, _markerIndex, NodeChanging);
}
Посмотрим, что здесь для чего. В строке 9 у всех созданных фигур по порядку вызывается метод DrawFigure(e.Graphics).
Напомню, что там:
Кликните здесь для просмотра всего текста
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// <summary>
/// Метод рисования фигуры по точкам базового списка
/// </summary>
/// <param name="graphics">"холст" - объект для рисования</param>
public virtual void DrawFigure(Graphics graphics)
{
    var points = Points.ToArray();
    if (Kind == DrawingKind.Polygon)
    {
        // заливаем фон кистью
        graphics.FillPolygon(Fill.Brush, points);
        // рисуем контур карандашом
        graphics.DrawPolygon(Stroke.Pen, points);
    }
    else
    {
        // рисуем контур карандашом
        graphics.DrawLines(Stroke.Pen, points);
    }
}

Здесь мы рисуем либо полигон, либо набор линий, в зависимости от настройки свойства Kind фигуры.
В строках с 10 по 50 мы рисуем маркеры на выбранных фигурах. Маркеры квадратные, по углам и на серединах
сторон, если специальный режим NodeChanging не включен (обычный режим для изменения размеров фигур
при помощи маркеров) и маркеры круглые, если включен режим NodeChanging изменения узлов. В этом режиме
мы можем менять положения узловых точек фигуры, настраивая её форму как нам вздумается.

В строках 53,54 для всех выбранных фигур вызывается метод DrawFocusFigure(), который будет вызываться при
начале "перетаскивания" маркеров, показывая будущую форму фигуры пунктирной линией.
Код метода DrawFocusFigure() будем разбирать, когда будем рассматривать пункт 3. Изменение размеров.

С размещением всё. Не будем нагружать более эту статью.
Ну а для нетерпеливых кладу полный код класса сюда: Stroke_Fill_Figure_Picture.zip

Продолжение следует...
Всего комментариев 2
Комментарии
  1. Старый комментарий
    Аватар для Storm23
    А окружности в вашем редакторе рисовать нельзя?
    Запись от Storm23 размещена 09.11.2018 в 21:41 Storm23 вне форума
  2. Старый комментарий
    Аватар для ashsvis
    Цитата:
    А окружности в вашем редакторе рисовать нельзя?
    Если окружность представить как правильный выпуклый многоугольник с достаточным количеством вершин, то можно.
    Если добавить поддержку кривых Безье, то окружность можно задавать шестью точками (где-то видел примерчик).
    Запись от ashsvis размещена 09.11.2018 в 21:56 ashsvis вне форума
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2019, vBulletin Solutions, Inc.