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

Шаг первый. Создаём рамку выбора для графического редактора.

Запись от ashsvis размещена 05.11.2018 в 10:41
Обновил(-а) ashsvis 05.11.2018 в 12:48

Когда речь заходит о всяческих полезняшках, например, о простом, встроенном в моё приложение,
графическом редакторе, у меня это вызывает некоторый трепет... Сколько возможностей открывается!
Игровой движок с редактором уровней, редактор пользовательских форм и так далее и тому подобное.

Первое, что приходит в голову при создании любого редактора, это средство выбора чего-либо на поверхности рисования,
или указания прямоугольной области, куда что-то нужно разместить. Или выбрать группу нарисованных объектов.

Итак, начнём!

Сначала разработаем класс и назовём его RectangleRibbonSelector. Экземпляр этого класса будет
создаваться пользователем в конструкторе формы, содержащей "холст" для рисования. Например,
компонент PictureBox, у которого есть поверхность для рисования, доступная через событие Paint, а также
он поддерживает реакцию на события нажатия кнопок и перемещения "мышки" при помощи событий MouseDown?
MouseMove и MouseUp.

Таким образом, нашему классу необходим конструктор, в который мы будем передавать этот "холст" через параметр
типа Control, определённого в пространстве имён System.Windows.Forms.
Кроме того, сразу же подумаем о том, что рамка выбора может иметь свои настраиваемые параметры для
цвета и толщины границ, и возможно, цвета заливки внутренней области.
Для этого определим ещё два необязательный параметра для карандаша и кисти.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <summary>
/// Конструктор с параметрами для перехвата и обработки событий
/// </summary>
/// <param name="container">визуальный компонент с поверхностью для рисования</param>
/// <param name="borderPen">карандаш для рисования рамки</param>
/// <param name="fillBrush">кисть для закрашивания рамки</param>
public RectangleRibbonSelector(Control container, Pen borderPen = null, Brush fillBrush = null)
{
    // запоминаем ссылку на контейнер для рисования
    _container = container;
    // по умолчанию рамка пунктирная, чёрного цвета
    _borderPen = borderPen ?? new Pen(Color.Black) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dot };
    _fillBrush = fillBrush;
    // подключаемся к необходимым событиям на контейнере
    _container.MouseDown += Control_MouseDown;
    _container.MouseMove += _control_MouseMove;
    _container.MouseUp += Control_MouseUp;
    _container.Paint += _control_Paint;
}
В теле конструктора запоминаем параметры в локальных переменных, а для переданного в параметре container
компонента подключаем свои обработчики событий для "мышки" и рисования.
Теперь к обработчикам событий.
Вот, что напишем в обработчике события рисования Paint:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <summary>
/// Обработчик события рисования на поверхности контейнера
/// </summary>
/// <param name="sender">визуальный компонент с поверхностью для рисования</param>
/// <param name="e">объект параметров события со свойством Graphics</param>
private void _control_Paint(object sender, PaintEventArgs e)
{
    // если прямоугольник выбора не пуст
    if (!_ribbonRect.IsEmpty)
    {   
        // если указана кисть заливки
        if (_fillBrush != null)
            e.Graphics.FillRectangle(_fillBrush, _ribbonRect); // то выполняем закрашивание
        // рисуем рамку прямоугольника выбора
        e.Graphics.DrawRectangle(_borderPen, _ribbonRect);
    }
}
У нас есть локальная переменная _ribbonRect типа Rectangle, которая настраивается в событиях перемещения "мышки",
а используется в методе события рисования. Если _ribbonRect имеет ширину и высоту, отличные от нуля,
то этот прямоугольник рисуется на канве контейнера _container, при помощи метода e.Graphics.DrawRectangle(...).

Метод обработки события нажатия левой кнопки "мышки":
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <summary>
/// Обработчик события нажатия кнопки мышки
/// </summary>
/// <param name="sender">визуальный компонент с поверхностью для рисования</param>
/// <param name="e">объект параметров события со свойством Location</param>
private void Control_MouseDown(object sender, MouseEventArgs e)
{
    // обрабатываем событие, если была нажата левая кнопка мышки
    if (e.Button == MouseButtons.Left)
    {
        // обнуление прямоугольника выбора
        _ribbonRect = Rectangle.Empty;
        // запоминаем точку первую точку выбора начала рисования прямоугольника выбора
        _mouseDownLocation = e.Location;
    }
}
В том методе мы запоминаем координаты нажатия для последующего использования в локальную переменную _mouseDownLocation, а локальная переменная _ribbonRect очищается.

Метод обработки события перемещения "мышки" с нажатой левой кнопкой:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// <summary>
/// Обработчик события перемещения мышки
/// </summary>
/// <param name="sender">визуальный компонент с поверхностью для рисования</param>
/// <param name="e">объект параметров события со свойством Location</param>
private void _control_MouseMove(object sender, MouseEventArgs e)
{
    // обрабатываем событие, если была нажата левая кнопка мышки
    if (e.Button == MouseButtons.Left)
    {
        // нормализация параметров для прямоугольника выбора
        // в случае, если мы "растягиваем" прямоугольник не только по "главной" диагонали
        _ribbonRect.X = Math.Min(_mouseDownLocation.X, e.Location.X);
        _ribbonRect.Y = Math.Min(_mouseDownLocation.Y, e.Location.Y);
        // размеры должны быть всегда положительные числа
        _ribbonRect.Width = Math.Abs(_mouseDownLocation.X - e.Location.X);
        _ribbonRect.Height = Math.Abs(_mouseDownLocation.Y - e.Location.Y);
        // запрашиваем контейнер для рисования, чтобы обновился
        _container.Invalidate();
    }
}
Сначала проверяем, что левая кнопка "мышки" нажата. Для рисования прямоугольника у нас есть теперь
две точки: начальная точка в переменной _mouseDownLocation и текущая точка, передаваемая нам в параметре
вызова этого события MouseEventArgs e.Location. Здесь также важно заметить, что разница между координатами X
(и координатами Y) этих двух точек должна быть положительной.
Это не так, если начальная точка нажатия "мышки" расположена ниже текущей точки курсора "мышки".
Поэтому нужно для координат X и Y прямоугольника _ribbonRect выбрать минимальные координаты из двух,
а для ширины и высоты прямоугольника взять абсолютное значение разницы между координатами двух точек.

Метод обработки события отпускания левой кнопки "мышки":
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
/// <summary>
/// Обработчик события отпускания кнопки мышки
/// </summary>
/// <param name="sender">визуальный компонент с поверхностью для рисования</param>
/// <param name="e">объект параметров события</param>
private void Control_MouseUp(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        // инициация события при окончании выбора прямоугольником
        if (!_ribbonRect.IsEmpty)
        {
            // создаём объект аргумента для возбуждения события
            RibbonSelectedEventArgs args = new RibbonSelectedEventArgs
            {
                // и передаём выбранный прямоугольник
                RectangleSelected = _ribbonRect
            };
            // возбуждаем событие окончания выбора
            OnRibbonSelected(args);
        }
        // обнуление прямоугольника выбора
        _ribbonRect = Rectangle.Empty;
        // запрашиваем контейнер для рисования, чтобы обновился
        _container.Invalidate();
    }
}
 
/// <summary>
/// Метод инициации события по окончании процесса выбора прямоугольником
/// </summary>
/// <param name="e">объект параметров события со свойством RectangleSelected</param>
protected virtual void OnRibbonSelected(RibbonSelectedEventArgs e)
{
    // если на событие подписались, то вызываем его
    OnSelected?.Invoke(this, e);
}
Когда мы отпускаем левую кнопку "мышки", не плохо бы передать это событие во внешний код,
где размещён экземпляр нашего класса RectangleRibbonSelector. Сначала проверяем, что прямоугольник
выбора не пуст, а потом вызываем метод OnRibbonSelected() с подготовленным объектом
параметра RibbonSelectedEventArgs e со свойством RectangleSelected, через которое и передаём
результирующий прямоугольник выбора.

Класс параметра выглядит так:
C#
1
2
3
4
public class RibbonSelectedEventArgs : EventArgs
{
    public Rectangle RectangleSelected { get; set; }
}
а поле события в нашем классе RectangleRibbonSelector определяется так:
C#
1
public event EventHandler<RibbonSelectedEventArgs> OnSelected;
Ну а теперь самое "вкусненькое":
Вот как будем использовать класс RectangleRibbonSelector в форме, содержащей контейнер для рисования:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public partial class FormMain : Form
{
    RectangleRibbonSelector ribbonSelector;
 
    public FormMain()
    {
        InitializeComponent();
        // создаём объект рамки выбора фигур
        ribbonSelector = new RectangleRibbonSelector(pictureBoxCanvas);
        ribbonSelector.OnSelected += RibbonSelector_OnSelected;
    }
 
    private void RibbonSelector_OnSelected(object sender, RibbonSelectedEventArgs e)
    {
        // пока что просто выводим в консоль координаты выбора прямоугольником
        Console.WriteLine(e.RectangleSelected);
    }
}
Ни какого лишнего кода в главной форме приложения!

Код класса RectangleRibbonSelector целиком:
Кликните здесь для просмотра всего текста

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
using System;
using System.Drawing;
using System.Windows.Forms;
 
namespace SimpleVectorGraphicsEditor
{
    public class RibbonSelectedEventArgs : EventArgs
    {
        public Rectangle RectangleSelected { get; set; }
    }
 
    public class RectangleRibbonSelector
    {
        private readonly Control _container;
        private readonly Pen _borderPen;
        private readonly Brush _fillBrush;
        private Point _mouseDownLocation = Point.Empty;
        private Rectangle _ribbonRect = new Rectangle();
 
        public event EventHandler<RibbonSelectedEventArgs> OnSelected;
 
        /// <summary>
        /// Конструктор с параметрами для перехвата и обработки событий
        /// </summary>
        /// <param name="container">визуальный компонент с поверхностью для рисования</param>
        /// <param name="borderPen">карандаш для рисования рамки</param>
        /// <param name="fillBrush">кисть для закрашивания рамки</param>
        public RectangleRibbonSelector(Control container, Pen borderPen = null, Brush fillBrush = null)
        {
            // запоминаем ссылку на контейнер для рисования
            _container = container;
            // по умолчанию рамка пунктирная, чёрного цвета
            _borderPen = borderPen ?? new Pen(Color.Black) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dot };
            _fillBrush = fillBrush;
            // подключаемся к необходимым событиям на контейнере
            _container.MouseDown += Control_MouseDown;
            _container.MouseMove += _control_MouseMove;
            _container.MouseUp += Control_MouseUp;
            _container.Paint += _control_Paint;
        }
 
        /// <summary>
        /// Обработчик события рисования на поверхности контейнера
        /// </summary>
        /// <param name="sender">визуальный компонент с поверхностью для рисования</param>
        /// <param name="e">объект параметров события со свойством Graphics</param>
        private void _control_Paint(object sender, PaintEventArgs e)
        {
            // если прямоугольник выбора не пуст
            if (!_ribbonRect.IsEmpty)
            {   
                // если указана кисть заливки
                if (_fillBrush != null)
                    e.Graphics.FillRectangle(_fillBrush, _ribbonRect); // то выполняем закрашивание
                // рисуем рамку прямоугольника выбора
                e.Graphics.DrawRectangle(_borderPen, _ribbonRect);
            }
        }
 
        /// <summary>
        /// Обработчик события нажатия кнопки мышки
        /// </summary>
        /// <param name="sender">визуальный компонент с поверхностью для рисования</param>
        /// <param name="e">объект параметров события со свойством Location</param>
        private void Control_MouseDown(object sender, MouseEventArgs e)
        {
            // обрабатываем событие, если была нажата левая кнопка мышки
            if (e.Button == MouseButtons.Left)
            {
                // обнуление прямоугольника выбора
                _ribbonRect = Rectangle.Empty;
                // запоминаем точку первую точку выбора начала рисования прямоугольника выбора
                _mouseDownLocation = e.Location;
            }
        }
 
        /// <summary>
        /// Обработчик события перемещения мышки
        /// </summary>
        /// <param name="sender">визуальный компонент с поверхностью для рисования</param>
        /// <param name="e">объект параметров события со свойством Location</param>
        private void _control_MouseMove(object sender, MouseEventArgs e)
        {
            // обрабатываем событие, если была нажата левая кнопка мышки
            if (e.Button == MouseButtons.Left)
            {
                // нормализация параметров для прямоугольника выбора
                // в случае, если мы "растягиваем" прямоугольник не только по "главной" диагонали
                _ribbonRect.X = Math.Min(_mouseDownLocation.X, e.Location.X);
                _ribbonRect.Y = Math.Min(_mouseDownLocation.Y, e.Location.Y);
                // размеры должны быть всегда положительные числа
                _ribbonRect.Width = Math.Abs(_mouseDownLocation.X - e.Location.X);
                _ribbonRect.Height = Math.Abs(_mouseDownLocation.Y - e.Location.Y);
                // запрашиваем контейнер для рисования, чтобы обновился
                _container.Invalidate();
            }
        }
 
        /// <summary>
        /// Обработчик события отпускания кнопки мышки
        /// </summary>
        /// <param name="sender">визуальный компонент с поверхностью для рисования</param>
        /// <param name="e">объект параметров события</param>
        private void Control_MouseUp(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)
            {
                // инициация события при окончании выбора прямоугольником
                if (!_ribbonRect.IsEmpty)
                {
                    // создаём объект аргумента для возбуждения события
                    RibbonSelectedEventArgs args = new RibbonSelectedEventArgs
                    {
                        // и передаём выбранный прямоугольник
                        RectangleSelected = _ribbonRect
                    };
                    // возбуждаем событие окончания выбора
                    OnRibbonSelected(args);
                }
                // обнуление прямоугольника выбора
                _ribbonRect = Rectangle.Empty;
                // запрашиваем контейнер для рисования, чтобы обновился
                _container.Invalidate();
            }
        }
 
        /// <summary>
        /// Метод инициации события по окончании процесса выбора прямоугольником
        /// </summary>
        /// <param name="e">объект параметров события со свойством RectangleSelected</param>
        protected virtual void OnRibbonSelected(RibbonSelectedEventArgs e)
        {
            // если на событие подписались, то вызываем его
            OnSelected?.Invoke(this, e);
        }
 
    }
}


Вот, пожалуй и всё для первого шага. Далее нужно будет создавать фигуры для рисования,
передавая в их конструктор начальный размер при помощи параметра e.RectangleSelected в
событии RibbonSelector_OnSelected.
Нужно будет создать класс-основу для таких фигур, а также контейнер для хранения фигур,
который будет хранить их, управлять ими и рисовать их на "холсте".
Всего комментариев 2
Комментарии
  1. Старый комментарий
    Аватар для Storm23
    Хорошее начало, ждемс продолжения.

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

    Цитата:
    Простой векторный графический редактор
    А почему-же простой? Давайте уж нормальный, со слоями, с undo/redo
    Запись от Storm23 размещена 05.11.2018 в 11:26 Storm23 вне форума
  2. Старый комментарий
    Аватар для ashsvis
    Цитата:
    нужно проверять какая кнопка мыши нажата
    Да, согласен. Поправил.
    Цитата:
    А почему-же простой?
    Поживём, увидим...
    Запись от ashsvis размещена 05.11.2018 в 12:56 ashsvis вне форума
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2019, vBulletin Solutions, Inc.