Создано при помощи нейросети Kandinsky

Рисуем фигуры на C# при помощи средств GDI+

User Rating: 0 / 5

Изображение к статье создано при помощи нейросети Kandinsky

Доброго времени суток, друзья.

В сегодняшней статье мы разберём с вами некоторые возможности GDI+ и научимся рисовать различные простые графические фигуры и примитивы на форме в приложении Windows Forms.

Описание того, что такое GDI+, можно найти по следующей ссылке на официальном сайте: https://learn.microsoft.com/ru-ru/dotnet/desktop/winforms/advanced/about-gdi-managed-code?view=netframeworkdesktop-4.8

Отмечу лишь кратко, что аббревиатура GDI обозначает Graphics Device Interface (т.е. интерфейс графического устройства), и по сути это часть операционной системы Windows, которая предоставляет возможность для создания векторных изображений и рисования. Также стоит упомянуть, что GDI+ является частью .NET Framework.

Итак, в рамках этой статьи мы рассмотрим возможности рисования при помощи средств GDI+ следующих фигур:

  • Прямоугольник (Rectangle) - нарисуем контур прямоугольника и закрашенный прямоугольник на форме
  • Эллипс (Ellipse) - нарисуем контур эллипса и закрашенный эллипс на форме
  • Полигон (Polygon) - нарисуем контур полигона и закрашенный полигон на форме
  • Дуга (Arc) - нарисуем на форме небольшой отрезок дуги. Дугой называется часть окружности или эллипса, заключённая между двумя заданными точками, принадлежащими этой окружности/эллипсу.
  • Сектор (Pie) - нарисуем на форме сектор и закрашенный сектор. Сектор похож на дугу, но отличается от неё тем, что две крайние точки дуги соединены радиальными линиями с центром окружности/эллипса.
  • Линии (Line) - нарисуем на форме 3 различных линии, каждая из которых будет иметь свой стиль штриха.
  • Фундаментальный сплайн (Curve) - мы нарисуем на форме несколько фундаментальных сплайнов. Три из них будут открытыми, и два из них будут закрытыми. О том, какие есть различия между открытыми и закрытыми фундаментальными сплайнами, мы посмотрим далее по тексту статьи.
  • Графический путь (GraphicsPath) - нарисуем на форме графический путь, состоящий из нескольких линий и дуги
  • Текст - выведем на форме текст, содержащийся в тестовой строковой переменной

Давайте сразу посмотрим на тот результат, который мы получим в конце статьи. Итак, у нас будет простое приложение для Windows Forms, с единственной главной формой, отображающейся при старте приложения. И на форме будут нарисованы перечисленные выше фигуры следующим образом:

 

В конце статьи вы сможете найти ссылку на архив с готовым примером проекта для среды Microsoft Visual Studio, который будет разобран в статье. А сейчас мы приступим к созданию приложения и по пути разберём нюансы при рисовании каждой из упомянутых фигур.

Создание нового проекта в среде Microsoft Visual Studio

Откройте среду разработки Microsoft Visual Studio и создайте новый проект с типом Приложение Windows Forms (.NET Framework), как показано ниже: 

 

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

Переименование главной формы приложения

После создания проекта будет создана главная форма по умолчанию с именем Form1 и соответствующий ей файл, содержащий код для класса формы, с именем Form1.cs.

Выберем в окне "Обозреватель решений" эту форму и в окне "Свойства" изменим название класса для формы на FrmGDIPaintingExample.cs.

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

Теперь установим главной форме некоторые значения свойств.

Установка свойств для главной формы приложения

В окне "Свойства" для главной формы установим следующие значения свойств:

  • Text - [Allineed.Ru] Пример рисования различных фигур при помощи GDI+
  • Size - 816; 489
  • StartPosition - CenterScreen

Все остальные свойства главной формы нашего приложения оставим с их значениями по умолчанию.

Теперь перейдем непосредственно к основной части - рисованию фигур на форме.

Обработка события Paint для главной формы

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

В результате в коде главной формы будет создан пустой обработчик для этого события:

        private void FrmGDIPaintingExample_Paint(object sender, PaintEventArgs e) {

        }

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

Первое, с чего мы начнём, - это добавим в код метода-обработчика для события Paint следующие две строки кода:

        private void FrmGDIPaintingExample_Paint(object sender, PaintEventArgs e) {
            Graphics g = e.Graphics;

            // Режим сглаживания выставляем в значение HighQuality (высокое качество)
            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;

        }

В первой добавленной строке кода мы записываем в переменную класса Graphics, которую назвали g, ссылку на экземпляр класса Graphics, который по умолчанию передаётся во втором параметре события Paint - у нас это параметр с именем e класса PaintEventArgs.

Этот экземпляр класса Graphics (как и сам класс Graphics) является, я бы сказал, основополагающим во всём процессе рисования различных фигур при помощи GDI+, поэтому мы непременно должны получить его для дальнейшей работы по рисованию чего бы то ни было на нашей главной форме. Класс Graphics, как мы увидим далее, предоставляет множество полезных методов для рисования самых разных фигур, и некоторые из этих методов мы как раз разберём в данной статье.

Вторая строка кода задаёт специальный режим сглаживания (smoothing mode) для экземпляра класса Graphics, который мы только что с вами получили. Этот режим сглаживания, как нетрудно догадаться, влияет на то, каким образом будут сглаживаться углы и сгибы фигур при их отрисовке. Если его не выставить, то по умолчанию некоторые линии, углы или сгибы фигур могут казаться немного "рубленными". Режим сглаживания управляется через свойство SmoothingMode класса Graphics, и в нашем случае мы его выставляем в значение HighQuality, что в дословном переводе означает "высокое качество". Вы можете заметить, что есть и другие значения в перечислении SmoothingMode, например AntiAlias, но, согласно официальной документации на это перечисление (см. ссылку https://learn.microsoft.com/ru-ru/dotnet/api/system.drawing.drawing2d.smoothingmode?view=dotnet-plat-ext-8.0) значения HighQuality и AntiAlias эквивалентны и по сути устанавливают сглаживание. Остальные значения для данного перечисления отключают сглаживание при отрисовке.

Более подробно про режим сглаживания и специфику его работы можно также прочитать на этой странице: https://learn.microsoft.com/ru-ru/dotnet/api/system.drawing.graphics.smoothingmode?view=dotnet-plat-ext-8.0

Далее мы перейдем к написанию кода для рисования фигур, но прежде я бы хотел сказать пару слов о специфике той системы координат, которая используется в GDI+.

Система координат

Из курса средней школы мы знаем привычную систему координат, которая выглядит так:

 

Т.е. в этой системе координат ось X идёт от начальной точки O(0; 0) вправо, а ось Y идёт от начальной точки вверх. Некоторая точка P с произвольными координатами (x0; y0) при этом лежит правее-выше начала системы координат.

В цифровом же мире компьютеров и программ (и, в частности, в GDI+) обычно принята несколько иная система координат, в которой ось Y идёт от начальной точки с координатами (0; 0) - вниз, и некоторая точка P(x0; y0) будет находиться правее-ниже начала системы координат:

Именно в контексте работы с GDI+ и в нашем примере получается, что начало системы координат находится в левом-верхнем углу формы приложения. И если не задано какое-то иное смещение системы координат (что, в целом, также можно осуществить, если есть в этом необходимость), то точка на "плоскости" главной формы с координатами (0; 0) находится в левом-верхнем углу формы. Точка (10; 0) находится на отдалении 10 пикселей по оси X (т.е. вправо) от начала координат и при этом на одном и том же уровне с начальной точкой, т.к. её координата Y равна нулю.

GDI+ поддерживает понятие разных типов систем координат и, как я сказал ранее, возможности перемещения системы координат в другую точку. Если интересны нюансы типов систем координат и возможности их смещения, то более подробно предлагаю ознакомиться читателю с данной темой, прочитав документацию по ссылке: https://learn.microsoft.com/ru-ru/dotnet/desktop/winforms/advanced/types-of-coordinate-systems?view=netframeworkdesktop-4.8

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

Теперь, когда мы поняли специфику системы координат в GDI+ и направления оси Y в ней, мы готовы к тому, чтобы начать отрисовку фигур на главной форме, для чего напишем соответствующий код на C#.

Рисуем на форме контур эллипса и закрашенный эллипс

Откроем код нашей главной формы и создадим в ней следующий метод:

        /// <summary>
        /// Отрисовка эллипсов (Ellipse). Один эллипс рисуется в виде контура через вызов метода DrawEllipse
        /// класса Graphics, второй эллипс - заполненный, рисуется через вызов метода FillEllipse
        /// класса Graphics.
        /// </summary>
        /// <param name="g">экземпляр класса Graphics, через который производить отрисовку объектов</param>
        private void DrawEllipse(Graphics g) {
            g.DrawEllipse(Pens.Red, new Rectangle(new Point(10, 10), new Size(100, 50)));
            g.FillEllipse(Brushes.Red, new Rectangle(new Point(10, 70), new Size(100, 50)));
        }

Как видим, он состоит всего из двух строк кода, в первой из которых мы вызываем метод DrawEllipse, а во второй - FillEllipse. Оба метода доступны в классе Graphics, который мы обсудили ранее, а входным параметром g у этого метода как раз выступает экземпляр класса Graphics. Разберём оба этих метода, у них есть некоторые отличия:

  • DrawEllipse - нарисует контур эллипса, без его "заливки" в цвет.
  • FillEllipse - нарисует эллипс с заливкой

Ввиду этой специфики, первый из методов принимает в первом своём параметре экземпляр класса Pen ("ручка"), а второй - экземпляр класса Brush ("кисть"). В GDI+, можно считать за правило, экземпляры класса Pen обычно используются для рисования линий и контуров, где не присутствует заливка, а экземпляры класса Brush, наоборот, в тех случаях, когда нужна и важна заливка некоторой области или фигуры. В качестве второго параметра для каждого из методов мы передаём новый экземпляр класса Rectangle ("прямоугольник"). Этот прямоугольник является описанным прямоугольником вокруг рисуемого эллипса. Т.е. фактически, для рисования эллипса мы задаём координаты и размеры описанного вокруг него прямоугольника. При создании экземпляра класса Rectangle первым аргументом передаётся экземпляр той точки (экземпляр класса Point), которая будет являться верхним-левым углом описанного вокруг эллипса прямоугольника. В нашем случае это точки с координатами (10; 10) и (10; 70). А вторым параметром мы передаём экземпляр класса Size, который задаёт двумя параметрами ширину и высоту этого прямоугольника. По координатам точек можно видеть, что закрашенный эллипс мы рисуем чуть ниже первого эллипса в виде обычного контура: его координата Y равна 70, а у первого - 10. Координата X у обоих эллипсов одинаковая и равна 10, и это значит, что они имеют одинаковый отступ слева от левого края главной формы. Как видите, всё довольно просто. Ну и заметьте также, что в методы DrawEllipse и FillEllipse мы передаём экземпляры класса Size с одними и теми же значениями ширины (100) и высоты (50) для обоих эллипсов, т.е. эллипсы будут у нас одинаковые по размерам.

Стоит также отметить, что экземпляры "ручек" и "кистей" для рисования контуров или закрашенных фигур можно как создавать вручную, так и пользоваться доступными через стандартные классы Pens и Brushes. В них есть множество экземпляров с различными цветами для рисования. В данном случае мы не создавали экземпляр ручки и кисти вручную (т.е. через оператор new), но далее в примерах мы это сделаем, поскольку ручное создание экземпляров ручек/кистей даёт некоторую большую гибкость, например по части стиля штриха рисуемых линий.

Сам по себе метод, созданный нами, не сильно полезен, пока никто его не вызывает, поэтому давайте теперь добавим его вызов в код метода-обработчика события Paint, который мы подготовили с вами ранее:

        private void FrmGDIPaintingExample_Paint(object sender, PaintEventArgs e) {
            Graphics g = e.Graphics;

            // Режим сглаживания выставляем в значение HighQuality (высокое качество)
            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;

            DrawEllipse(g);
        }

Обратите внимание, что мы передаём в наш новый метод уже имеющийся экземпляр класса Graphics - переменную с именем g. Это значит, что наш новый метод DrawEllipse(Graphics g) получит готовый экземпляр класса Graphics и будет использовать его для рисования эллипсов.

На этом этапе мы готовы запустить наше приложение и уже увидеть первый результат! Можем это сделать, нажав клавишу F5 или кнопку "Пуск" в среде разработки.

Результат после запуска нашей программы должен получиться такой:

 

Поздравляю! Вот мы и нарисовали наши самые первые фигуры на форме при помощи средств GDI+! Это уже неплохой результат, но впереди мы познакомимся ещё и с другими интересными возможностями.

Рисуем на форме контур прямоугольника и закрашенный прямоугольник

Аналогично тому, как мы поступили с эллипсами, давайте посмотрим, как нарисовать прямоугольники на главной форме нашего приложения.

Для этого в код главной формы добавим следующий новый метод DrawRectangles:

        private void DrawRectangles(Graphics g) {
            Pen greenDottedPen = new Pen(Brushes.Green, 2f);
            greenDottedPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot;

            g.DrawRectangle(greenDottedPen, new Rectangle(new Point(120, 10), new Size(100, 50)));

            greenDottedPen.Dispose();

            g.FillRectangle(Brushes.Green, new Rectangle(new Point(120, 70), new Size(100, 50)));
        }

В этом методе точно так же мы определили входным параметром с именем g экземпляр класса Graphics, который будет использоваться для рисования прямоугольников.

Далее мы создаём новый экземпляр класса Pen, передавая ему в первом аргументе конструктора зелёную кисть (Brushes.Green), а во втором - толщину рисуемой линии контура прямоугольника, равную 2f. 2f - это число типа float, поскольку второй параметр конструктора для класса Pen ожидает именно тип данных float. 2f эквивалентно записи 2.0f, т.е. по сути это толщина линии равная 2. При желании вы можете поэкспериментировать и передать что-то вида 2.5f и так далее.

Дальше мы задаём свойство DashStyle для нашего экземпляра "ручки", равным System.Drawing.Drawing2D.DashStyle.Dot. Это означает, что ручка будет рисовать пунктирными линиями (т.е. основанными на близко расположенных друг к другу точках), а весь контур нашего прямоугольника также будет пунктирным.

Далее идёт вызов метода DrawRectangle, который доступен в классе Graphics. Он, по аналогии с тем, как мы рисовали эллипсы, рисует контур прямоугольника, без заливки. Первым параметром передаём нашу "ручку", а вторым - экземпляр класса Rectangle, который и задаёт местоположение прямоугольника и его размеры.

Дальше идёт очень важная строка кода, которая позволяет высвободить ресурсы, занимаемые вручную созданным объектом Pen: greenDottedPen.Dispose()

Обращаю внимание, что в случаях, когда кисти/ручки создаются вручную - через оператор new - их нужно обязательно освобождать, когда они выполнили свою функцию для отрисовки и становятся "не нужны". Этот момент позволит экономить ресурсы компьютера, на котором выполняется приложение и своевременно освобождать память, занимаемую объектами Pen и Brush.

В конце метода идёт отрисовка заполненного прямоугольника при помощи вызова метода FillRectangle. Заметьте, что как и в случае с эллипсами "Fill-методы" принимают первым параметром экземпляр "кисти" (Brush), а "Draw-методы" принимают экземпляр "ручки" (Pen). Такова их специфика, и она прослеживается и при других Draw/Fill методах.

Снова добавим вызов нашего нового метода DrawRectangles к обработчику события Paint - сразу после вызова DrawEllipse(g):

        private void FrmGDIPaintingExample_Paint(object sender, PaintEventArgs e) {
            Graphics g = e.Graphics;

            // Режим сглаживания выставляем в значение HighQuality (высокое качество)
            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;

            DrawEllipse(g);
            DrawRectangles(g);
        }

Снова запустим наше приложение и увидим следующий результат:

 

Как видите, теперь на форме отобразились два прямоугольника, как мы и хотели. Также обратим внимание на то, что в отличие от контура эллипса контур прямоугольника - пунктирный и толщина линии равна 2. Это результат того, что мы вручную сконфигурировали экземпляр класса Pen на свой вкус.

Рисуем на форме контур полигона и закрашенный полигон

Прежде, чем мы начнём отрисовку полигонов на нашей форме, давайте поймем, что в принципе представляет собой полигон, и какова его специфика. Полигон (или, по сути, знакомый нам многоугольник) в GDI+ - это набор точек с определёнными координатами, где последовательные пары точек соединяются линиями, образуя контур полигона:

 

Например, в этом полигоне у нас 7 точек P1, P2, P3, ..., P7, где координаты первой точки - (x1; y1), координаты второй - (x2; y2) и так далее. Последовательные пары точек P1-P2, P2-P3, P3-P4, P4-P5, P6-P7 соединяются линиями, образуя контур полигона. Закрашенный полигон - это когда внутренняя область полигона, определённого точками будет закрашена определённой "кистью" (Brush). Чтобы задать полигон нам нужно передать методам GDI+, доступным в классе Graphics, массив точек нашего полигона. И в случае, если мы рисуем лишь контур, передать дополнительно экземпляр "ручки" (Pen), которой необходимо отрисовать контур нашего полигона, а в случае закрашенного полигона - экземпляр "кисти" (Brush).

Создадим в коде главной формы следующий метод DrawPolygons:

        /// <summary>
        /// Отрисовка полигонов (Polygon). Один полигон рисуется в виде контура через вызов метода DrawPolygon 
        /// класса Graphics, второй полигон - заполненный, рисуется через вызов метода FillPolygon
        /// класса Graphics.
        /// </summary>
        /// <param name="g">экземпляр класса Graphics, через который производить отрисовку объектов</param>
        private void DrawPolygons(Graphics g) {
            Pen blueDashedPen = new Pen(Brushes.Blue, 1.5f);
            blueDashedPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;

            g.DrawPolygon(blueDashedPen, new Point[] {
                new Point(230, 10),
                new Point(370, 30),
                new Point(310, 50),
                new Point(240, 50)
            });

            blueDashedPen.Dispose();

            g.FillPolygon(Brushes.Blue, new Point[] {
                new Point(230, 70),
                new Point(370, 90),
                new Point(310, 110),
                new Point(240, 110)
            });
        }

В первых двух строках метода мы вручную создаём экземпляр "ручки" (Pen) с именем переменной blueDashedPen, и наша "ручка" строится на базе синей кисти (Brushes.Blue) и имеет ширину 1.5 пикселя. Мы также устанавливаем ей стиль штриха (свойство DashStyle), равный System.Drawing.Drawing2D.DashStyle.Dash. Это значит, что контур нашего полигона будет рисоваться не сплошной синей линией, а синей штриховой линией с шириной 1.5 пикселя.

Далее мы вызываем метод DrawPolygon у экземпляра g класса Graphics, в который передаём этот экземпляр "ручки", а также, вторым аргументом вызова, передаём массив точек, состоящий из 4-х элементов. Каждый элемент массива - это экземпляр класса Point, который задаёт очередную вершину полигона. 

Также обратите внимание, что сразу после вызова метода мы освобождаем ресурсы, занимаемые вручную созданным экземпляром Pen, вызывая blueDashedPen.Dispose()

Далее мы рисуем закрашенный полигон при помощи вызова метода FillPolygon у экземпляра g класса Graphics, также состоящий из 4-х точек, у которого координата X для всех точек - идентична координате X у предыдущего полигона, а координату Y мы смещаем на +60 пикселей, т.е. 2-й закрашенный полигон будет нарисован чуть ниже первого. Также замечаем, что для "Fill-метода" в первом аргументе вызова метода идёт экземпляр "кисти", (а не "ручки", как обычно у "Draw-методов"), и в данном случае мы не создаём "кисть" вручную, а переиспользуем доступный экземпляр синей кисти - Brushes.Blue.

Теперь можем добавить вызов нашего нового метода DrawPolygons в обработчик события Paint, после двух ранее добавленных вызовов для отрисовки эллипсов и прямоугольников: 

        private void FrmGDIPaintingExample_Paint(object sender, PaintEventArgs e) {
            Graphics g = e.Graphics;

            // Режим сглаживания выставляем в значение HighQuality (высокое качество)
            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;

            DrawEllipse(g);
            DrawRectangles(g);
            DrawPolygons(g);
        }

Запустим снова программу, нажав F5 или кнопку "Пуск" на панели инструментов среды разработки, мы должны увидеть следующий результат:

Как видим, оба наших полигона (многоугольника) были успешно отрисованы.

Рисуем на форме дугу

Дуга (Arc) представляет собой часть эллипса (или окружности), которая соединяет две точки этого эллипса (или окружности). Рассмотрим, к примеру, вариант, когда дуга рисуется по эллипсу (пример рисования дуги по окружности будет у нас как раз в коде, и там всё аналогично - просто в случае окружности вокруг неё описан квадрат, с одинаковыми сторонами, а не прямоугольник, как в случае эллипса). Для рисования дуги в GDI+ нужны будут два очень важных параметра (в аргументах вызова метода DrawArc, при помощи которого рисуется дуга, они передаются последними, как мы увидим чуть далее по коду):

  • startAngle (тип данных float) - угол, в градусах, который измеряется по часовой стрелке, начиная от оси X и заканчивая начальной точкой дуги.
  • sweepAngle (тип данных float) - угол, в градусах, который измеряется по часовой стрелке, начиная от значения параметра startAngle и заканчивая конечной точкой дуги.

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

Начальная точку дуги мы получаем, двигаясь по часовой стрелке от отметки "0 градусов" на указанное нами количество градусов - в данном примере 180 градусов (жёлтая нижняя пунктирная стрелка показывает направление этого движения). Т.е. параметр startAngle в этом примере равен 180.

Конечную точку дуги мы получим, продолжая двигаться по часовой стрелке на заданное нами количество градусов (жёлтая пунктирная стрелка сверху показывает направление этого движения). В данном примере мы продолжим двигаться на 90 градусов по часовой стрелке от начальной точки и получим конечную точку дуги. Таким образом, параметр sweepAngle в нашем случае равен 90, и он определяет расположение конечной точки нашей дуги.

 

В нашем примере в метод DrawArc, предоставляемый нам средствами GDI+, мы передадим одинаковые значения ширины и высоты для описанного вокруг эллипса прямоугольника (Rectangle), а это значит, что мы будем рисовать дугу не по эллипсу, а по окружности. Но, как я уже упомянул выше, с точки зрения механики вычисления начальной/конечной точек дуги отличий при этом особо никаких нет.

Итак, напишем в коде нашей главной формы следующий короткий метод DrawArc (одноимённый с тем, как он называется в классе Graphics), который принимает на вход параметр g класса Graphics (по аналогии со всеми рассмотренными нами выше методами для отрисовки эллипсов, прямоугольников и полигонов):

        /// <summary>
        /// Метод рисует дугу (Arc), которая обрамлена в прямоугольник с координатами верхнего-левого угла
        /// (380; 10) и размерами ширины и высоты, равными 50 пикселей.
        /// </summary>
        /// <param name="g">экземпляр класса Graphics, через который производить отрисовку объектов</param>
        private void DrawArc(Graphics g) {
            g.DrawArc(Pens.Orange, new Rectangle(new Point(380, 10), new Size(50, 50)), 180, 90);            
        }

Первым аргументом при вызове метода GDI+ мы передаём экземпляр "ручки" (Pen) оранжевого цвета (Pens.Orange), вторым аргументом - экземпляр прямоугольника, описанного вокруг эллипса, третьим и четвёртым параметром - значения для startAngle и sweepAngle, которые мы рассмотрели выше. Поскольку при создании экземпляра прямоугольника мы передали new Size(50, 50), то это означает, что ширина и высота прямоугольника равны 50 пикселям, т.е. это, фактически, квадрат. А вписанный в этот квадрат эллипс является, фактически, окружностью в нашем примере.

Добавим теперь вызов нашего метода в обработчик события Paint - после вызова DrawPolygons(g):

        private void FrmGDIPaintingExample_Paint(object sender, PaintEventArgs e) {
            Graphics g = e.Graphics;

            // Режим сглаживания выставляем в значение HighQuality (высокое качество)
            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;

            DrawEllipse(g);
            DrawRectangles(g);
            DrawPolygons(g);
            DrawArc(g);
        }

Теперь запустим нашу программу и видим следующий результат:

 

Заметим справа от синего контура полигона оранжевую дугу, которую мы только что нарисовали.

Рисуем на форме два сектора (Pies)

Сектор (Pie) - это фигура очень схожая с дугой, разница лишь в том, что в случае сектора начальная и конечная точка дуги соединяются линиями (называемыми радиальными) с центром эллипса, из которого получена сама дуга.

Если рассмотреть предыдущую иллюстрацию и соединить начальную и конечную точки дуги с центром эллипса, то мы получим сектор:

Давайте напишем метод DrawPies в коде главной формы, который нарисует контур сектора и закрашенный сектор. Контур сектора получается при помощи вызова метода DrawPie класса Graphics, а закрашенный сектор получаем при помощи вызова метода FillPie, который также есть в классе Graphics:

        /// <summary>
        /// Отрисовка секторов (Pie). Один сектор рисуется в виде контура через вызов метода DrawPie
        /// класса Graphics, второй сектор - заполненный, рисуется через вызов метода FillPie
        /// класса Graphics.
        /// </summary>
        /// <param name="g">экземпляр класса Graphics, через который производить отрисовку объектов</param>
        private void DrawPies(Graphics g) {
            g.DrawPie(Pens.Magenta, new Rectangle(new Point(440, 10), new Size(100, 50)), 180, 90);
            g.FillPie(Brushes.Magenta, new Rectangle(new Point(440, 70), new Size(100, 50)), 180, 90);
        }

Снова обратим внимание, что в метод DrawPie первым параметром передаётся готовый экземпляр "ручки" (Pens.Magenta), а в случае метода FillPie первый параметр - готовый экземпляр кисти (Brushes.Magenta). Вторым параметром у обоих методов идёт передача экземпляра прямоугольника, который описан вокруг эллипса, из которого будет выделен сектор. Последними параметрами передаются startAngle и sweepAngle - как и у метода DrawArc, что мы уже разобрали выше.

Добавим теперь вызов нашего метода DrawPies в метод-обработчик для события Paint:

        private void FrmGDIPaintingExample_Paint(object sender, PaintEventArgs e) {
            Graphics g = e.Graphics;

            // Режим сглаживания выставляем в значение HighQuality (высокое качество)
            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;

            DrawEllipse(g);
            DrawRectangles(g);
            DrawPolygons(g);
            DrawArc(g);
            DrawPies(g);
        }

Теперь запустим снова приложение, чтобы увидеть результат. Как видим, после дуги отрисовались два сектора - один незаполненный, второй - заполненный:

 

Рисуем на форме несколько линий (Lines)

Линия - это, пожалуй, одна из самых простых фигур, которые можно рисовать при помощи средств GDI+. Для её отрисовки нужен экземпляр "ручки" (Pen), а также указание двух точек (экземпляры класса Point), которые являются концами линии.

Добавим в код главной формы следующие методы: главный метод DrawLines для рисования трёх разных линий и три отдельных метода на каждый из типов линий - DrawSolidCrimsonLine, DrawDashedCrimsonLine и DrawDashedDotCrimsonLine:

        /// <summary>
        /// Отрисовка линий (Line). Рисуется 3 разных линии с различным стилем штриха.
        /// </summary>
        /// <param name="g">экземпляр класса Graphics, через который производить отрисовку объектов</param>
        private void DrawLines(Graphics g) {
            DrawSolidCrimsonLine(g);
            DrawDashedCrimsonLine(g);
            DrawDashedDotCrimsonLine(g);
        }

        /// <summary>
        /// Отрисовка обычной линии (без заданного стиля штриха).
        /// </summary>
        /// <param name="g">экземпляр класса Graphics, через который производить отрисовку объектов</param>
        private void DrawSolidCrimsonLine(Graphics g) {
            g.DrawLine(Pens.Crimson, new Point(550, 10), new Point(650, 50));
        }

        /// <summary>
        /// Отрисовка линии со стилем штриха - "штриховая" (Dash).
        /// </summary>
        /// <param name="g">экземпляр класса Graphics, через который производить отрисовку объектов</param>
        private void DrawDashedCrimsonLine(Graphics g) {
            Pen dashedCrimsonPen = new Pen(Brushes.Crimson, 2);
            dashedCrimsonPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;

            g.DrawLine(dashedCrimsonPen, new Point(550, 20), new Point(650, 60));

            dashedCrimsonPen.Dispose();
        }

        /// <summary>
        /// Отрисовка линии со стилем штриха - "штрихпунктирная" (DashDot).
        /// </summary>
        /// <param name="g">экземпляр класса Graphics, через который производить отрисовку объектов</param>
        private void DrawDashedDotCrimsonLine(Graphics g) {
            Pen dashedDotCrimsonPen = new Pen(Brushes.Crimson, 3);
            dashedDotCrimsonPen.DashStyle = System.Drawing.Drawing2D.DashStyle.DashDot;

            g.DrawLine(dashedDotCrimsonPen, new Point(550, 30), new Point(650, 70));

            dashedDotCrimsonPen.Dispose();
        }

Как видим, главный метод DrawLines вызывает три других метода. Каждый из методов определяет экземпляр "ручки" (Pen) на основе малинового цвета кисти  (Brushes.Crimson). Обратите внимание, что в методе DrawSolidCrimsonLine, который рисует обычную сплошную линию по двум точкам не требуется ничего лишнего - достаточно вызвать метод DrawLine у экземпляра g класса Graphics и передать ему две точки. Это самый простой способ рисования линии, без дополнительных параметров стиля штриха у линии.

В двух других методах - DrawDashedCrimsonLine и DrawDashedDotCrimsonLine мы создаём экземпляры "ручки" вручную через оператор new, а также дополнительно устанавливаем значения для свойства DashStyle у экземпляра "ручки". В первом случае линия у нас будет пунктирной ( что достигается указанием значения стиля штриха DashStyle равным System.Drawing.Drawing2D.DashStyle.Dash), во втором случае - штрихпунктирной (значение System.Drawing.Drawing2D.DashStyle.DashDot).

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

Теперь добавим вызов главного метода DrawLines в метод-обработчик для события Paint:

        private void FrmGDIPaintingExample_Paint(object sender, PaintEventArgs e) {
            Graphics g = e.Graphics;

            // Режим сглаживания выставляем в значение HighQuality (высокое качество)
            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;

            DrawEllipse(g);
            DrawRectangles(g);
            DrawPolygons(g);
            DrawArc(g);
            DrawPies(g);
            DrawLines(g);
        }

Запускаем наше приложение, чтобы увидеть отрисованные на главной форме линии:

 

Рисуем на форме строку с текстом

Давайте теперь научимся рисовать строку с текстом при помощи средств, предоставляемых GDI+. Для отрисовки строки с текстом используется метод DrawString, доступный в классе Graphics. Ему необходимо передать непосредственно строку с текстом, экземпляр шрифта, который необходимо использовать для вывода строки, экземпляр "кисти" (Brush), а также координаты левого-верхнего угла прямоугольника, который описан вокруг строки текста с учётом её размеров.

Создадим в коде главной формы метод DrawTestString и напишем следующий код:

        /// <summary>
        /// Отрисовка тестовой строки внизу формы.
        /// </summary>
        /// <param name="g">экземпляр класса Graphics, через который производить отрисовку объектов</param>
        private void DrawTestString(Graphics g) {
            Font font = new Font("Arial", 14);
            string testString = "Пример рисования фигур средствами GDI+";
            SizeF sizeOfTestString = g.MeasureString(testString, font);

            g.DrawString(testString, font, Brushes.Purple, new PointF(Width / 2 - sizeOfTestString.Width / 2, Height - Height / 4));
        }

Мы задали переменную font, создав экземпляр шрифта и указав его параметры, далее мы определили переменную testString с тестовой строкой для вывода на форме. Также мы используем метод GDI+ MeasureString, который очень полезен, когда требуется измерить строку, т.е. понять её высоту и ширину при отрисовке. Метод требует двух параметров - сама строка и экземпляр шрифта для отрисовки и возвращает результат с типом данных SizeF. В нашем случае мы записали размер измеренной строки в переменную sizeOfTestString, она пригодится нам в следующей строке для вывода строки на экран.

Ключевой метод для вывода строки - это DrawString, в который мы передаём все необходимые параметры: сама строка (testString), экземпляр шрифта (font), экземпляр "кисти" - в нашем случае это фиолетовая готовая кисть (Brushes.Purple), далее - экземпляр точки типа PointF (его отличие от типа Point в том, что координаты x и y задаются как значения типа float, а не int).

Мы выводим нашу строку по центру относительно горизонтали и примерно на 1/4 высоты главной формы, ближе к нижнему краю формы.

Осталось добавить наш новый метод опять к обработчику события Paint:

        private void FrmGDIPaintingExample_Paint(object sender, PaintEventArgs e) {
            Graphics g = e.Graphics;

            // Режим сглаживания выставляем в значение HighQuality (высокое качество)
            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;

            DrawEllipse(g);
            DrawRectangles(g);
            DrawPolygons(g);
            DrawArc(g);
            DrawPies(g);
            DrawLines(g);
            DrawTestString(g);
        }

Теперь запустим приложение для проверки результата. Наша строка отобразилась на главной форме:

 

Рисуем на форме фундаментальные сплайны (Curves)

Фундаментальный сплайн (Curve) - это последовательность отдельных кривых, которые при объединении образуют более крупную кривую. Более подробное объяснение специфики фундаментальных сплайнов можно найти по следующей ссылке: https://learn.microsoft.com/ru-ru/dotnet/desktop/winforms/advanced/cardinal-splines-in-gdi?view=netframeworkdesktop-4.8

Ключевой, на мой взгляд, момент для понимания и рисования фундаментальных сплайнов - это то, что они строятся на основании переданного массива точек, а также для их отрисовки важен параметр натяжения (tension) между точками. Натяжение, равное 0, превратит сплайн по сути в последовательность прямых, соединяющих все точки. Это объясняется тем, что значение 0 представляет собой как бы "бесконечно сильное физическое натяжение", и это натяжение превращает кривые, соединяющие точки, в прямые. Если же мы будем увеличивать значение натяжения, к примеру, укажем его равным 1, то натяжение между точками будет "слабее", т.е. мы будем получать более плавные изгибы вокруг каждой точки.

Также фундаментальные сплайны в GDI+ можно рисовать открытыми и закрытыми. В открытом фундаментальном сплайне крайние его точки не соединены вместе. Закрытый же сплайн, как и следует из его названия, "закрывает" кривую, т.е. соединяет крайние её точки, образуя закрытый контур кривой.

Лучше всего это понять на практике, поэтому давайте создадим в коде главной формы метод DrawCurves для отрисовки нескольких фундаментальных сплайнов с разными значениями параметра натяжения.

        /// <summary>
        /// Метод рисует несколько фундаментальных сплайнов (Curve) на форме.
        /// </summary>
        /// <param name="g">экземпляр класса Graphics, через который производить отрисовку объектов</param>
        private void DrawCurves(Graphics g) {
            g.DrawCurve(Pens.LimeGreen, new Point[] {
                new Point(10, 150),
                new Point(30, 190),
                new Point(80, 240),
                new Point(10, 300),
            }, 1);

            g.DrawCurve(Pens.LimeGreen, new Point[] {
                new Point(40, 150),
                new Point(60, 190),
                new Point(110, 240),
                new Point(40, 300),
            }, 2);

            g.DrawCurve(Pens.LimeGreen, new Point[] {
                new Point(70, 150),
                new Point(90, 190),
                new Point(140, 240),
                new Point(70, 300),
            }, 3);

            g.DrawClosedCurve(Pens.LimeGreen, new Point[] {
                new Point(180, 210),
                new Point(200, 220),
                new Point(240, 240),
                new Point(180, 300),
            }, 3, System.Drawing.Drawing2D.FillMode.Alternate);

            g.DrawClosedCurve(Pens.LimeGreen, new Point[] {
                new Point(330, 210),
                new Point(350, 220),
                new Point(400, 240),
                new Point(330, 300),
            }, 3, System.Drawing.Drawing2D.FillMode.Alternate);

        }

Теперь добавим вызов этого метода в метод-обработчик для события Paint главной формы:

        private void FrmGDIPaintingExample_Paint(object sender, PaintEventArgs e) {
            Graphics g = e.Graphics;

            // Режим сглаживания выставляем в значение HighQuality (высокое качество)
            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;

            DrawEllipse(g);
            DrawRectangles(g);
            DrawPolygons(g);
            DrawArc(g);
            DrawPies(g);
            DrawLines(g);
            DrawTestString(g);
            DrawCurves(g);
        }

Запустим нашу программу и увидим следующий результат:

 

Мы нарисовали 3 открытых фундаментальных сплайна (три зелёных кривых слева) с разным параметром натяжения, который меняли от 1 до 3. Видно, что степень "натяжения" между точками фундаментальных сплайнов изменяется при их отрисовке: кривые выглядят по-разному. Далее идёт отрисовка двух других фундаментальных сплайнов, которые являются уже закрытыми. Закрытые сплайны в методе мы нарисовали при помощи вызова метода DrawClosedCurve, доступном в классе Graphics. Открытые сплайны - при помощи метода DrawCurve. Оба метода принимают первым параметром экземпляр "ручки" (Pen) для рисования, далее - массив точек, входящих в фундаментальный сплайн, и третий параметр - это tension, т.е. натяжение, которое мы обсудили. Метод DrawClosedCurve также требует передачи 4-го параметра FillMode, определяющего способ заполнения кривой. Этот параметр является обязательным, однако он не обрабатывается.

Рисуем на форме графический путь (Path)

В GDI+ помимо рассмотренных нами выше фигур ещё есть и интересная возможность объединения различных фигур в графический путь (GraphicsPath). Фактически, мы можем добавить различные фигуры (линии, прямоугольники, дуги и т.д.) в единый путь, который соединит эти фигуры в единое целое. Графический путь представляется классом GraphicsPath, и у него есть различные методы по добавлению в путь фигур. Затем полученный графический путь мы можем передать в метод DrawPath класса Graphics, а также передать в его первом аргументе экземпляр "ручки" (Pen), которой следует нарисовать полученный путь. 

Создадим в коде главной формы метод DrawPath и, как обычно, зададим в качестве его входного параметра переменную g класса Graphics:

        /// <summary>
        /// Метод создаёт экземпляр класса GraphicsPath и добавляет в него различные графические объекты
        /// (4 линии и одну дугу) с последующей их отрисовкой через вызов метода DrawPath класса Graphics.
        /// </summary>
        /// <param name="g">экземпляр класса Graphics, через который производить отрисовку объектов</param>
        private void DrawPath(Graphics g) {
            GraphicsPath path = new GraphicsPath();
            path.AddLine(new Point(450, 230), new Point(450, 210));
            path.AddLine(new Point(450, 210), new Point(550, 210));
            path.AddArc(new Rectangle(550, 110, 100, 100), 180, 180);
            path.AddLine(new Point(650, 210), new Point(750, 210));
            path.AddLine(new Point(750, 210), new Point(750, 230));
            path.CloseFigure();
            
            Pen penForPath = new Pen(Brushes.BurlyWood, 2);
            penForPath.DashStyle = DashStyle.Dash;

            g.DrawPath(penForPath, path);

            penForPath.Dispose();
        }

Как видим, мы сначала создаём новый экземпляр класса GraphicsPath и присваиваем его переменной path. Дальше мы добавляем в наш путь 2 линии (AddLine), 1 дугу (AddArc) и две другие линии, после чего вызываем метод CloseFigure, которая позволяет закрыть текущую полученную фигуру. Мы также создаём вручную экземпляр "ручки" и записываем его в переменную penForGraph, а затем устанавливаем ему стиль штриха.

Наконец, мы вызываем метод DrawPath у экземпляра класса Graphics для отрисовки графического пути, после чего очищаем ресурсы, занятие экземпляром "ручки", поскольку он более нам не нужен.

Опять добавим вызов нашего нового метода в метод-обработчик события Paint:

        private void FrmGDIPaintingExample_Paint(object sender, PaintEventArgs e) {
            Graphics g = e.Graphics;

            // Режим сглаживания выставляем в значение HighQuality (высокое качество)
            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;

            DrawEllipse(g);
            DrawRectangles(g);
            DrawPolygons(g);
            DrawArc(g);
            DrawPies(g);
            DrawLines(g);
            DrawTestString(g);
            DrawCurves(g);
            DrawPath(g);
        }

Снова запустим наше приложение и посмотрим на получившийся результат:

Мы можем заметить, что справа от фундаментальных сплайнов была нарисована замкнутая фигура, которая объединяет в себе 4 линии, одну дугу и представляет скомпонованный нами графический путь.

Стоит сказать, что возможности GDI+ на этом не ограничиваются, и в нём есть другие интересные методы в классе Graphics, которые позволяют рисовать на форме иконки (DrawIcon) изображения (DrawImage), кривую Безье (DrawBezier), последовательность из нескольких линий (DrawLines), а также набор прямоугольников (DrawRectangles). Думаю, что имея ту основу, что мы разобрали с вами в текущей статье, можно будет без особого труда разобраться с тем, как работают и эти интересные методы, но я оставлю это за рамками текущей статьи.

Если вам понравилась статья и вы бы хотели увидеть продолжение (разбор методов DrawIcon, DrawImage и т. д.) - поставьте лайки или напишите комментарии, так я пойму ваш интерес к теме GDI+ и его возможностей.

Ну а пока всё, спасибо за внимание и успехов в работе с GDI+. Внизу вы найдете ссылку на полностью готовый архив с примером, что мы вместе с вами разобрали в рамках статьи. Можете сверить код примера с тем, что получилось у вас.

Ссылка на готовый пример: https://allineed.ru/our-products/download/4-allineed-ru-examples/29-csharp-gdiplus-example

 

Яндекс.Метрика