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

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

Запись от ashsvis размещена 05.11.2018 в 10:41
Показов 7382 Комментарии 2

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

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

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

Сначала разработаем класс и назовём его 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 вне форума
 
Новые блоги и статьи
Саморегулирующийся социальный контракт для сервера cross-section.
Hrethgir 14.08.2026
С кодом конечно таких глубоких размышлений пока не было, впрочем я уже привык к алгоритмизации. Суть предмета записи: снова в диалоге с нейросетью (я взял пока себе ник для учётки админа - Rector). . . .
Часы электронные
Uhbif79 12.08.2026
Выкладываю программу часов. Программа позволяет: 1. Использовать системное время и дату, 2. Есть возможность вводить время и дату вручную. 3. Реализованы 2 будильника: начало и конец рабочего дня. . . .
Часы с будильником на основе класса QLCDNumber
Uhbif79 12.08.2026
Всем добрый день, выкладываю программу часов с будильником на основе класса QLCDNumber. Здесь я пробовал самостоятельно создавал классы, впервые столкнулся с видимостью переменной одного класса из. . .
Установка MinGW GCC 16.2 и CMake
8Observer8 10.08.2026
VK Видео: https:/ / vkvideo. ru/ video-240781534_456239017 YouTube: eY5-5PyI9NM Текстовая версия
Неделя из жизни имитационной модели склада: мои кривые руки растут, откуда надо
anaschu 10.08.2026
Неделя из жизни имитационной модели склада: как я почти написал неправильную логику и что с этим делать Работаю сейчас над учебно-рабочим проектом: строю в AnyLogic имитационную модель процессов. . .
Калькулятор для расчета родства
russiannick 07.08.2026
1. Задача: Создать калькулятор для расчета родства. Родственных связей существует 8 ступеней, такие как: p - отец P - мать q - муж Q - жена b - брат B - сестра s - сын S - дочь
Мир по моей воле
kumehtar 07.08.2026
Когда-то кажется, что всё просто. Ты весь такой светлый. Причиняешь добро. Борешься за справедливость в этом тёмном мире. Потом начинаешь замечать одну неприятную вещь. Почти каждый хороший. . .
Кредитный калькулятор
Maks 05.08.2026
Решение задачи по прикладной информатике средствами 1С. Задача: Напишите приложение-калькулятор, которое помогает рассчитывать параметры кредита для аннуитетного и дифференцированного видов. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru