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

Шаг 7 (графический векторный редактор) Версия 0.1

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

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

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

Я подготовил небольшой демо проект для демонстрации и обсуждения: SimpleVectorGraphicsEditor Demo V0_1.zip

Проект состоит из шести классов редактора, всё остальное необходимо для демонстрации работы.
На центральной части формы расположена панель типа Panel, у которой размер Dock = Dock.Fill привязан ко внешней таблице
компоновки и установлено свойство AutoSize = true. На этой панели расположен компонент PictureBox, который
и будет контейнером для нашего редактора. В свойство PictureBox BackgroundImage загружена картинка, имитирующая
координатную сетку. Наш редактор подключается в конструкторе формы:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public partial class FormMain : Form
{
    private readonly PictureEditor _editor;
    private const string Title = @"Простой векторный графический редактор (демо)";
 
    public FormMain()
    {
        InitializeComponent();
        // создаём хранилище созданных фигур, которое также и рисует их
        _editor = new PictureEditor(pbCanvas)
        {
            BackgoundContextMenu = cmsBkgPopup,
            FigureContextMenu = cmsFigPopup
        };
        _editor.FigureSelected += EditorFigureSelected;
        _editor.EditorFarConnerUpdated += EditorFarConnerUpdated;
        cbWidth.Items.Clear();
        for (var i = 1; i < 61; i++) cbWidth.Items.Add(i.ToString("0"));
    }
....
При создании экземпляра редактора PictureEditor в конструктор передаётся ссылка на контейнер PictureBox pbCanvas,
затем подключаются два контекстных меню:

pbCanvas.BackgoundContextMenu = cmsBkgPopup - для случая, когда ни одна фигура не выбрана (для общих настроек)
pbCanvas.FigureContextMenu = cmsFigPopup - для вызова на выбранных фигурах.

Содержимое контекстных меню подготавливается в дизайнере VS, а наш редактор только управляет из вызовом.
Обработчики всех пунктов контекстных меню и главного меню также пишутся и подключаются в дизайнере VS.

Редактор формирует два события: при выборе фигуры и при изменении расположения фигур, когда они перетаскиваются.
Внешний обработчик синхронизируют внешние для редактора компоненты формы. Размер контейнера изменяется при
изменении положения фигур в редакторе.
[здесь хочу отступ]Во внешнем обработчике этого события размер контейнера подгоняется под текущий размер поля редактора,
а панель, на которой находится контейнер, отображает полосы прокрутки, если это необходимо. На данный момент жесткий
размер поля редактирования не предусмотрен. Хотя он нужен, например, для проверок на возможность размещения.

Для начала рисования фигур пользователь должен выбрать фигуру, нажав на значок с её изображением. При нажатии на
любую из этих кнопочек выполняется обработчик:
C#
1
2
3
4
5
6
7
8
9
10
11
private void tsbSelectMode_Click(object sender, EventArgs e)
{
    foreach (ToolStripButton btn in tsFigures.Items) btn.Checked = false;
    ((ToolStripButton)sender).Checked = true;
    if (tsbPolyline.Checked)
        _editor.EditorMode = EditorMode.AddLine;
    else if (tsbPolygon.Checked)
        _editor.EditorMode = EditorMode.AddPolygon;
    else
        _editor.EditorMode = EditorMode.Selection;
}
В строке 3 мы сбрасываем все флажки, а потом устанавливаем отметку в кнопочке нажатия (строка 4).
Далее переключаем режим редактора в зависимости от выбора пользователя. Пока всего только две фигуры можно нарисовать.
Это полилиния и полигон.

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

Дополнительно я тут подготовил небольшие "плюшки", как то копирование и вырезание выделенных фигур в буфер обмена,
вставку фигур из буфера обмена. Любопытно, что эта штука работает, даже если мы скопируем фигуры в одном приложении
редактора, а вставим в другом запущенном приложении редактора.
Другой "плюшкой" является поддержка возможности отмены действий редактирования и возврат после отмены. Для этой
цели создан класс-помощник StackMemory:
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
[Serializable]
public class StackMemory
{
    readonly int _stackDepth; // глубина стека
 
    readonly List<byte[]> _list = new List<byte[]>();
        
    /// <summary>
    /// Конструктор
    /// </summary>
    /// <param name="depth"></param>
    public StackMemory(int depth)
    {
        _stackDepth = depth;
        if (depth < 1) _stackDepth = 1;
        _list.Clear();
    }
 
    /// <summary>
    /// Помещаем данные в стек
    /// </summary>
    /// <param name="stream"></param>
    public void Push(MemoryStream stream)
    {
        if (_list.Count > _stackDepth) _list.RemoveAt(0);
        _list.Add(stream.ToArray());
    }
 
    /// <summary>
    /// Очищаем стек
    /// </summary>
    public void Clear()
    {
        _list.Clear();
    }
 
    /// <summary>
    /// Количество сохранённых версий в стеке
    /// </summary>
    public int Count
    {
        get { return (_list.Count); }
    }
 
    /// <summary>
    /// Извлечение данных из стека
    /// </summary>
    /// <param name="stream"></param>
    public void Pop(MemoryStream stream)
    {
        if (_list.Count <= 0) return;
        var buff = _list[_list.Count - 1];
        stream.Write(buff, 0, buff.Length);
        _list.RemoveAt(_list.Count - 1);
    }
}
В конструкторе класса задаётся "глубина" отката и реализованы основные методы для работы со стековой структурой.
Применение этого класса также довольно простое, сначала создаем два стека Undo и Redo:
C#
1
2
readonly StackMemory _undoStack = new StackMemory(100);
readonly StackMemory _redoStack = new StackMemory(100);
Затем будем использовать в свойстве редактора FileChanged:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// <summary>
/// Признак изменения данных.
/// ВНИМАНИЕ! Для правильной работы логики Undo|Redo
/// изменение этого свойства производить ДО изменения данных!
/// </summary>
public bool FileChanged
{
    get
    {
        return (_fileChanged);
    }
    set
    {
        _fileChanged = value;
        PrepareToUndo(_fileChanged);
        PrepareToRedo(false);
    }
}
Для отката действий необходимо запомнить состояние редактора ДО изменения его коллекции фигур, поэтому когда
предстоит что-то изменить, например, закончили перетаскивание и перед тем как сместить фигуры, нужно присвоить
свойству FileChanged значение True. В сеттере выполняться два метода подготовки для Undo и Redo.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <summary>
/// Подготовка к отмене (сохранения состояния)
/// </summary>
/// <param name="changed">True - cохранить состояние</param>
private void PrepareToUndo(bool changed)
{
    if (changed)
    {
        using (var stream = new MemoryStream())
        {
            SaveToStream(stream);
            _undoStack.Push(stream);
        }
    }
    else
        _undoStack.Clear();
}
Метод для Redo такой же, только используется _redoStack. Методом SaveToStream() записываем всё в поток, а потом
помещаем этот поток в стек. Вот код метода SaveToStream():
C#
1
2
3
4
5
6
7
8
9
10
11
12
/// <summary>
/// Сохранить все фигуры в поток
/// </summary>
/// <param name="stream">поток в памяти</param>
/// <param name="listToSave">список для сохранения</param>
private void SaveToStream(Stream stream, List<Figure> listToSave = null)
{
    var formatter = new BinaryFormatter();
    var list = (listToSave ?? _figures).ToList();
    formatter.Serialize(stream, list);
    stream.Position = 0;
}
Возврат состояния происходит при при вызове метода UndoChanges():
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// <summary>
/// Отмена действий, изменений
/// </summary>
public void UndoChanges()
{
    if (!CanUndoChanges) return;
    PrepareToRedo(true);
    _selected.Clear();
    _figures.Clear();
    GC.Collect();
    using (var stream = new MemoryStream())
    {
        _undoStack.Pop(stream);
        var list = LoadFromStream(stream);
        foreach (var fig in list) _figures.Add(fig);
    }
    _container.Invalidate();
}
Вот, вкратце, что я имел сообщить по поводу строительства простого графического редактора. Далее ставим мачты, паруса можем дополнять код редактора всякими инструментами, например:
  • Выравнивание групп фигур по сторонам и серединам во вертикали и горизонтали
  • Группировка фигур, чтобы вместе держались при перемещении и изменении размеров с общим размерным прямоугольником
  • Реализация редакторов отдельных свойств фигуры (я сделал только для цвета и толщины линии)
  • Было бы здорово сделать поддержку кривых Безье
  • Добавить к фигурам текст
  • Добавить маркеры привязки, чтобы можно было цеплять одну фигуру к другой
  • Добавить слои
  • Добавить экспорт в другие графические форматы (векторные и не векторные)
  • ... (делайте свои предложения)

Большое спасибо за внимание! Буду благодарен за ваши советы, указание на ошибки (которые буду исправлять по
возможности) и наставления.
Всего комментариев 4
Комментарии
  1. Старый комментарий
    Аватар для ashsvis

    Как думаете?

    Есть предложение код классов поместить в dll, а код формы в UserControl и тоже в dll. Так будет удобнее таскать из проекта в проект...
    Запись от ashsvis размещена 13.11.2018 в 14:28 ashsvis вне форума
  2. Старый комментарий
    Аватар для Storm23
    Работает нормально (плюс-минус), но вот техническая реализация - не очень.
    Много повторяющегося кода, примитивное undo/redo (без команд), объектная модель рассыпется как только нужно будет добавить больше функционала. А функционал очень бедный. Даже нет окружности, не говоря уже о более сложном.

    Цитата:
    дополнять код редактора всякими инструментами
    Сначала бы неплохо было бы добавить элементарный набор фигур, хотя бы уровня Paint.
    Запись от Storm23 размещена 13.11.2018 в 20:23 Storm23 вне форума
  3. Старый комментарий
    Аватар для ashsvis
    undo/redo (без команд) - дайте ссылочку, где посмотреть по-подробнее.
    функционал очень бедный - у меня нет цели "переплюнуть" векторные редакторы на рынке. Нужен только необходимый функционал. А "мясо" нарастает со временем. Главное, чтобы это было кому-то нужным.
    Даже нет окружности - да не успел я пока, надо перенести из проектов...

    О переносе маркеров в фигуры. - если переносим маркеры, то и рисовать себя фигура должна сама. Я так думаю.
    Цитата:
    добавить элементарный набор фигур
    Сделаю.
    Запись от ashsvis размещена 13.11.2018 в 20:36 ashsvis вне форума
  4. Старый комментарий
    Аватар для Storm23
    Цитата:
    undo/redo (без команд) - дайте ссылочку, где посмотреть по-подробнее.
    Паттерн команда. Также здесь.
    Цитата:
    Нужен только необходимый функционал. А "мясо" нарастает со временем.
    Дело в том, что у вас нельзя нарастить мясо. Модель не позволяет. Посмотрите как у вас отрисовка происходит:
    C#
    1
    2
    3
    4
    
                    if (kind == DrawingKind.Polygon)
                        graphics.DrawPolygon(pen, points); // рисование контура
                    else
                        graphics.DrawLines(pen, points);
    И таких фрагментов по программе раскидано десяток. Если, допустим, вы добавите отрисовку окружностей, вы во всех местах будете писать такой паровоз:
    C#
    1
    2
    3
    4
    5
    6
    7
    
                    if (kind == DrawingKind.Polygon)
                        graphics.DrawPolygon(pen, points); // рисование контура
                    else
                    if (kind == DrawingKind.Circle)
                        graphics.DrawEllipse(pen, ...); // рисование окружности
                    else
                        graphics.DrawLines(pen, points);
    Во всех тринадцати местах?

    Я потому особо и не критиковал предыдущий код, потому что его придется почти полностью переписать, как только вы решите нарастить функционал. Поэтому текущий код все равно в мусорку уйдет, и рассматривать его нет смысла.
    Ну а если ограничится рисованием только прямоугольников, то можно наверное оставить как есть... И так сойдет

    Цитата:
    Главное, чтобы это было кому-то нужным.
    Исходите из аксиомы, что это никому не нужно
    Я таких статей тут десяток накропал, отдача - почти нулевая.
    Этот форум для студентов-двоечников. Максимум что они сделают - это скачают ваш проект и сдадут как лабу. Это все.
    Запись от Storm23 размещена 13.11.2018 в 21:19 Storm23 вне форума
    Обновил(-а) Storm23 13.11.2018 в 21:23
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2019, vBulletin Solutions, Inc.
Рейтинг@Mail.ru