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

Шаг второй (продолжение). Спутники базового класса.

Запись от ashsvis размещена 07.11.2018 в 14:07

Продолжаем разрабатывать простой векторный графический редактор.
Я обещал подробно разобрать классы для хранения настроек рисования Stroke и Fill, так теперь приступим:

Начнём с класса попроще.
Класс Stroke предназначен для хранения и предоставления базовому классу Drawing информации о том, как
рисовать линию контура замкнутой фигуры или саму линию фигуры не замкнутой.
Конструкторы класса Stroke выглядят так:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <summary>
/// Конструктор без параметров, со свойствами по умолчанию
/// </summary>
public Stroke() : this(Color.Black, 1.0F) { }
        
/// <summary>
/// Конструктор с параметрами
/// </summary>
/// <param name="color">Цвет линии</param>
/// <param name="width">Ширина линии</param>
public Stroke(Color color, Single width)
{
    Color = color;
    Width = width;
    DashStyle = DashStyle.Solid;
    Alpha = 255;
}
Первый конструктор без параметров, создает объект Stroke с настройками по умолчанию, как то:
цвет линии чёрный, толщиной в 1 единицу.

Второй конструктор имеет два параметра, позволяющие задать при создании объекта два наиболее используемых
свойства - цвет и толщину линии. В конструкторе также задаётся тип линии DashStyle.Solid - сплошная, и значение
для канала яркости максимальное - 255 единиц. Настройка канала яркости позволяет создавать полупрозрачные
границы фигуры, что может понадобиться в некоторых случаях.

Свойства класса:
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
/// <summary>
/// Цвет линии фигуры
/// </summary>
public Color Color { get; set; }
        
/// <summary>
/// Ширина линии фигуры
/// </summary>
public Single Width { get; set; }
        
/// <summary>
/// Стиль линии фигуры
/// </summary>
public DashStyle DashStyle { get; set; }
        
/// <summary>
/// Яркость:
/// 0 - полностью прозрачный,
/// 255 - полноцветный
/// </summary>
public int Alpha { get; set; }
        
/// <summary>
/// Стиль начала линии
/// </summary>
public LineCap StartCap { get; set; }
        
/// <summary>
/// Стиль конца линии
/// </summary>
public LineCap EndCap { get; set; }
        
/// <summary>
/// Стиль соединения двух отрезков линии
/// </summary>
public LineJoin LineJoin { get; set; }
позволят также настроить тип линии (пунктиром, штрих-пунктиром), тип отображения концов незамкнутых линий,
и тип отображения углов фигуры, что хорошо видно при больших значениях толщины линий.

Имеется также полезный метод присваивания свойств из другого объекта этого типа:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// <summary>
/// Метод присваивания свойств другого stroke
/// </summary>
/// <param name="stroke">Источник значений свойств stroke</param>
public void Assign(Stroke stroke)
{
    if (stroke == null) return;
    Color = stroke.Color;
    Width = stroke.Width;
    DashStyle = stroke.DashStyle;
    Alpha = stroke.Alpha;
    LineJoin = stroke.LineJoin;
    StartCap = stroke.StartCap;
    EndCap = stroke.EndCap;
}
а для реализации интерфейса ICloneable класс содержит метод Clone(), в котором и используется метод Assing():
C#
1
2
3
4
5
6
7
8
9
10
/// <summary>
/// Реализация интерфейса ICloneable
/// </summary>
/// <returns>Возвращаем копию Stroke</returns>
public object Clone()
{
    var stroke = new Stroke();
    stroke.Assign(this);
    return stroke;
}
Когда приходит время воспользоваться преимуществами применения такого класса настройки "карандаша",
мы будем использовать метод Pen(), который создаёт графический объект класса System.Drawing.Pen и делает ему
все необходимые настройки:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// <summary>
/// Метод возвращает "карандаш", настроенный по текущим свойствам stroke
/// </summary>
public Pen Pen()
{
    var pen = new Pen(Color.FromArgb(Alpha, Color))
        {
            Width = Width,
            LineJoin = LineJoin,
            StartCap = StartCap,
            EndCap = EndCap
        };
    if (DashStyle == DashStyle.Custom)
    {
        pen.DashStyle = DashStyle.Solid;
        pen.Color = Color.Transparent;
    }
    else
        pen.DashStyle = DashStyle;
    return (pen);
}
Здесь сразу замечу, что применять такой метод следует при помощи обёртки using, чтобы гарантированно освобождать
системные ресурсы (примерчик):
C#
1
2
3
// рисуем контур карандашом
using (var pen = Stroke.Pen())
    graphics.DrawPolygon(pen, points);
Следующий класс Fill предназначен для хранения и использования настроек при закрашивании поверхности
фигуры с замкнутым контуром.

Вот конструкторы класса Fill:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <summary>
/// Конструктор без параметров, с цветом по умолчанию
/// </summary>
public Fill() : this(Color.White) { }
 
/// <summary>
/// Конструктор с параметром
/// </summary>
/// <param name="color">Цвет заливки фона</param>
public Fill(Color color)
{
    Color = color;
    Alpha = 255;
    PatternColor = Color.Black;
    PatternIndex = 1; // FillMode.Solid;
    Mode = FillMode.Solid;
    HatchMode = HatchStyle.Percent50;
    LinearMode = LinearGradientMode.Horizontal;
}
Тут конструктор без параметров ссылается на второй конструктор с параметром, чтобы настроить основное свойство
фона - цвет фона. Все остальные поля класса настраиваются на часто употребляемые начальные значения.

Теперь свойства:
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
/// <summary>
/// Цвет заливки
/// </summary>
public Color Color { get; set; }
 
/// <summary>
/// Яркость:
/// 0 - полностью прозрачный,
/// 255 - полноцветный
/// </summary>
public int Alpha { get; set; }
 
/// <summary>
/// Цвет линий при заливке выбранной битовой маской
/// </summary>
public Color PatternColor { get; set; }
 
/// <summary>
/// Стиль заполнения битовой маской
/// </summary>
public HatchStyle HatchMode { get; set; }
 
/// <summary>
/// Стиль заполнения градиентной кистью
/// </summary>
public LinearGradientMode LinearMode { get; set; }
Поверхность может закрашиваться несколькими способами, для управления этим
служит свойство:
C#
1
2
3
4
/// <summary>
/// Режим заполнения поверхности фигуры
/// </summary>
public FillMode Mode { get; set; }
где тип этого свойства FillMode описан как:
C#
1
2
3
4
5
6
7
8
9
10
/// <summary>
/// Перечисление для типа заполнения
/// </summary>
public enum FillMode
{
    None = 0,           // без заливки
    Solid = 1,          // сплошная заливка
    Hatch = 2,          // битовая маска из набора
    LinearGradient = 3  // линейный градиент
};
Вот главный (рисующий) метод Brush(), в котором можно посмотреть, как используется свойство Mode:
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
/// <summary>
/// Метод возвращает "кисть", настроенный по текущим свойствам fill
/// </summary>
/// <param name="rect">прямоугольная область для заливки градиентом</param>
/// <returns>Настроенная кисть для заполнения контура фигуры</returns>
public Brush Brush(RectangleF rect)
{
    Brush brush = null;
    switch (Mode)
    {
        case FillMode.None:
            // нет заливки, берём прозрачный цвет
            brush = new SolidBrush(Color.Transparent);
            break;
        case FillMode.Solid:
            // сплошная заливка, берём цвет и канал яркости
            brush = new SolidBrush(Color.FromArgb(Alpha, Color));
            break;
        case FillMode.Hatch:
            brush = new HatchBrush(HatchMode,
                                    Color.FromArgb(Alpha, PatternColor),
                                    Color.FromArgb(Alpha, Color));
            break;
        case FillMode.LinearGradient:
            brush = new LinearGradientBrush(rect, Color.FromArgb(Alpha, PatternColor),
                                            Color.FromArgb(Alpha, Color), LinearMode);
            break;
    }
    return (brush);
}
Для штриховой заливки используется системная кисть типа HatchBrush, для градиентной заливки
используется системная кисть типа LinearGradientBrush, для простого фона - кисть SolidBrush.
И каждая из кистей требует отдельного свойства для настройки.

Настраивать кисть, имея столько параметров - не просто. Поэтому, для управления всем этим хозяйством,
создано свойство поддержки индекса типа заливки:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <summary>
/// Свойство для настройки fill по индексу шаблона,
/// применяемого для заливки.
/// Имена всех шаблонов возвращает метод string[] GetAllPatternNames()
/// </summary>
public int PatternIndex
{
    get
    {
        return (_patternIndex);
    }
    set
    {
        _patternIndex = value;
        Mode = FillModeFromIndex(_patternIndex);
        HatchMode = HatchStyleFromIndex(_patternIndex);
        LinearMode = LinearGradientModeFromIndex(_patternIndex);
    }
}
В этом свойстве принимаемое значение сквозного индекса, раскладывается по индексам, специфическим к
своим кистям. Для пользователя создан вспомогательный метод, возвращающий текстовые названия всех
шаблонов:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <summary>
/// Метод, возвращающий имена всех доступных шаблонов для заливки контура фигуры
/// </summary>
/// <returns>Массив имён шаблонов заливки</returns>
public static string[] GetAllPatternNames()
{
    var hatchNameArray = Enum.GetNames(typeof(HatchStyle));
    var linearGradientNameArray = Enum.GetNames(typeof(LinearGradientMode));
    var n = 2 + LinearGradientModeCount + HatchStyleCount;
    var names = new string[n];
    names[0] = "Прозрачный";
    names[1] = "Сплошной";
    var i = 2;
    for (var j = 0; j < LinearGradientModeCount; j++)
        names[i++] = linearGradientNameArray[j];
    for (var j = 0; j < HatchStyleCount; j++)
        names[i++] = hatchNameArray[j];
    return (names);
}
Можно заполнить строками этого метода компонент ComboBox на форме редактора и пользователь,
выбирая из списка, будет указывать свойству PatternIndex индекс для настройки по выбранному шаблону:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// <summary>
/// Метод определения типа заливки по индексу шаблона заливки
/// </summary>
/// <param name="index">применяемый индекс шаблона заливки</param>
/// <returns>тип заполнения</returns>
public static FillMode FillModeFromIndex(int index)
{
    var fillmode = FillMode.None;
    if (IsNonePatternIndex(index)) fillmode = FillMode.None;
    else if (IsSolidPatternIndex(index)) fillmode = FillMode.Solid;
    else if (IsLinearGradientPatternIndex(index)) fillmode = FillMode.LinearGradient;
    else if (IsHatchPatternIndex(index)) fillmode = FillMode.Hatch;
    return (fillmode);
}
Для поддержки градиентной кисти служат следующие методы:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <summary>
/// Метод для вычисления стиля градиентной заливки из индекса шаблона
/// </summary>
/// <param name="index">применяемый индекс шаблона заливки</param>
/// <returns>Возвращает стиль градиентной заливки</returns>
public static LinearGradientMode LinearGradientModeFromIndex(int index)
{
    checked
    {
        return (LinearGradientMode)(index - 2);
    }
}
 
// Вспомогательный массив со стилями направлений градиентной заливки
static readonly LinearGradientMode[] LinearGradientModeArray =
    (LinearGradientMode[])Enum.GetValues(typeof(LinearGradientMode));
 
// Длина вспомогательного массива со стилями направлений градиентной заливки
static readonly int LinearGradientModeCount = LinearGradientModeArray.Length;
Для поддержки кисти для битовой (штриховой) заливки служат следующие методы:
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
/// <summary>
/// Метод для вычисления стиля битовой заливки
/// </summary>
/// <param name="index">применяемый индекс шаблона заливки</param>
/// <returns></returns>
public static HatchStyle HatchStyleFromIndex(int index)
{
    checked
    {
        return (HatchStyle)(index - 2 - LinearGradientModeCount);
    }
}
 
/// <summary>
/// Метод для определения того факта, что применяется битовая заливка
/// </summary>
/// <param name="index">применяемый индекс шаблона заливки</param>
/// <returns>True - заливка по битовой маске</returns>
public static bool IsHatchPatternIndex(int index)
{
    checked
    {
        var idx = index - 2 - LinearGradientModeCount;
        return ((idx >= 0) && (idx < HatchStyleCount));
    }
}
 
// Вспомогательный массив со стилями битовой заливки по маске
static readonly HatchStyle[] HatchStyleArray = (HatchStyle[])Enum.GetValues(typeof(HatchStyle));
 
// Длина вспомогательного массива со стилями битовой заливки по маске
static readonly int HatchStyleCount = HatchStyleArray.Length - 3;
Для поддержки сплошной заливки (наиболее часто используемой) служат следующие методы:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <summary>
/// Метод для определения того факта, что заливка не применяется
/// </summary>
/// <param name="index">применяемый индекс шаблона заливки</param>
/// <returns>True - заливки нет</returns>
public static bool IsNonePatternIndex(int index)
{
    return (index == 0);
}
 
/// <summary>
/// Метод для определения того факта, что применяется сплошная заливка
/// </summary>
/// <param name="index">применяемый индекс шаблона заливки</param>
/// <returns>True - сплошная заливка</returns>
public static bool IsSolidPatternIndex(int index)
{
    return (index == 1);
}
Ну и на закуску. Два метода для поддержки копирования объектов типа Fill:
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
/// <summary>
/// Метод присваивания свойств другого fill
/// </summary>
/// <param name="fill">Источник значений свойств fill</param>
public void Assign(Fill fill)
{
    if (fill == null) return;
    Color = fill.Color;
    Alpha = fill.Alpha;
    PatternColor = fill.PatternColor;
    PatternIndex = fill.PatternIndex;
    Mode = fill.Mode;
    HatchMode = fill.HatchMode;
    LinearMode = fill.LinearMode;
}
 
/// <summary>
/// Реализация интерфейса ICloneable
/// </summary>
/// <returns>Возвращаем копию Fill</returns>
public object Clone()
{
    var fill = new Fill();
    fill.Assign(this);
    return fill;
}
Да, что-то многовато материала на этот раз... Но это всё нужно и будет работать на благо построения
нашего простого векторного графического редактора!

На следующем шаге я планирую описать класс DrawingEditor, в котором будут применены
все наши наработки на этот момент и который будет управлять размещением и размерами потомков базового
класса Drawing. До встречи!
Всего комментариев 3
Комментарии
  1. Старый комментарий
    Аватар для Storm23
    Смотрите.
    1) Ваши классы Stroke и Fill просто дублируют Pen и Brush. Они не вносят никакого функционала, а потому не нужны.
    2) Методы типа такого:

    C#
    1
    2
    3
    4
    5
    6
    7
    
    public static HatchStyle HatchStyleFromIndex(int index)
    {
        checked
        {
            return (HatchStyle)(index - 2 - LinearGradientModeCount);
        }
    }
    Напрягают. Что за индекс? Зачем индекс? Почему он int? Что это вообще тут делает? Что такое 2? Я даже не хочу вникать зачем оно нужно, понятно что это костыль.

    3) Метод Assign напрягает изначально, и плохо пахнет. Может ваша семантика предполагает, что это struct? Тогда все эти Assign ушли бы сами собой.

    4) Далее, вот этот методы Pen и Brush и необходимость использовать using для них сделаны неправильно. Во-первых, они неправильно названы, должно быть GetPen и GetBrush. Во-вторых, возвращать IDisposable объекты из методов - как бы не очень хорошая идея, потому что за их уничтожение возлагается ответственность внешнего класса. Лучше бы Pen был свойством класса Stroke, а сам Stroke был бы IDisposable, и уничтожал бы Pen в своем Dispose(). Ну и кроме того, такой бы код работал быстрее, потому что не нужно было бы создавать Pen и Brush при каждой отрисовке.

    5) Далее, и в предыдущем классе Drawing (кстати его логичнее было бы назвать Polygon) и в текущих Stroke и Fill вы передаете аргументы в конструктор. Это плохо. В конструктор нужно передавать те аргументы, которые не будут меняться на протяжении жизни объекта. Но ведь они у вас будут меняться, потому что они зависят от действий пользователя.
    Какой же смысл передавать их в конструктор?
    Да и вообще я бы вынес их из Polygon (ой, Drawing). Полигон должен хранить набор точек и возвращать GraphicsPath, а отрисовкой бы занимался другой класс, который бы уже знал про Pen и Brush. Полигону же про это знать не обязательно. Понимаете, хранение точек и отрисовка это разные задачи, и лучше их сделать отдельно. Но то таке...

    Кроме того, в конструктор вообще нужно передавать данных поменьше. Потому что конструктор можно вызвать только один раз и если он выполняет роль инициализатора, то второй раз инициализировать новыми значениями strike и fill - не получится, ведь второй раз конструктор не вызовешь.

    6) Вот этому точно не место в базовом классе:
    C#
    1
    2
    3
    4
    5
    6
    7
    8
    
    public static string[] GetAllPatternNames()
    {
        var hatchNameArray = Enum.GetNames(typeof(HatchStyle));
        var linearGradientNameArray = Enum.GetNames(typeof(LinearGradientMode));
        var n = 2 + LinearGradientModeCount + HatchStyleCount;
        var names = new string[n];
        names[0] = "Прозрачный";
        names[1] = "Сплошной";
    Это - элементы имеющие отношение к интерфейсу, им здесь не место.
    Запись от Storm23 размещена 07.11.2018 в 15:27 Storm23 на форуме
  2. Старый комментарий
    Аватар для ashsvis
    Спасибо за комментарий. Попытаюсь объяснить, что мною двигало, когда создавал такие классы:
    1) Я предполагал (вернее существует некое приложение, где это выполнено), что настройка свойств для
    рисования будет происходит в отдельных формах. Своя форма для Stroke и своя для Fill. Формы эти общие для
    любого объекта от класса Drawing.
    Форма "Свойства линии фигуры"
    Форма "Свойства фона фигуры"
    Поэтому я посчитал, что нужны классы - оболочки над Pen и Bush.
    2) Такие методы появились от попытки соединить несколько настроек в единый список, и поскольку, первые два места
    настройки заливки были заняты под "Прозрачный" и "Сплошной", то стили с градиентами занимают 4 места, а далее -
    стили со штриховкой. Отсюда эта ужасная волшебная цифра 2.
    3) Да, мне нужен был метод, который тупо копирует все поля класса, и без всякой сериализации (это устарело?)
    4) Согласен, буду дорабатывать.
    5) Это тоже надо осмыслить. Дайте время. По поводу названия, Polygon я хотел использовать как имя класса наследника
    для замкнутых фигур. Хотя фигуру можно как замкнуть, так и разомкнуть, превратив её в линию. Вообщем, Drawing наверное
    не совсем удачное название, но точно не Polygon.
    6) Да, не место, но индекс так сильно связан с этим списком стилей и не охота его потерять. Да, нужно это править.
    Запись от ashsvis размещена 07.11.2018 в 19:15 ashsvis вне форума
  3. Старый комментарий
    Аватар для Storm23
    По поводу пунктов 1, 2, 6 и возможно 3:
    Все дело в том, что вы завязываетесь на пользовательский интерфейс.
    При разработке базисного движка нужно сосредоточится на функционале движка. Вы же думаете не о движке, а о том как вы сделаете GUI. А это неправильно. Модель не должна зависеть от интерфейса. Если что-то плохо ложится на интерфейс, это проблемы интерфейса, а не движка. Значит нужно будет разработать адаптеры для связки вашего UI и движка. Но засовывать явно интерфейсные вещи в движок - неправильно.

    3) Не то что бы устарело, но я бы использовал либо сериализацию, либо struct. Почему? Потому что вы забудете в Assign присвоить какое-то поле, и при клонировании будет неуловимый баг.

    5) Обратите внимание, что хранение точек (или любого графического контента вообще) и отрисовка - это разные задачи. И из принципа единственной ответственности, лучше разнести этот функционал по разным классам.
    Запись от Storm23 размещена 08.11.2018 в 03:14 Storm23 на форуме
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2018, vBulletin Solutions, Inc.
Рейтинг@Mail.ru