Основы C#

Работа с графикой в C#: простой пример

Обновлено: · 5 мин чтения

Простейшая графика в C# - это рисование линий, фигур и текста через класс Graphics из System.Drawing в приложении Windows Forms или на объекте Bitmap. В статье три рабочих примера, анимация на таймере, сохранение картинки в файл и сравнение с WPF, SkiaSharp и ImageSharp, которые нужны вне Windows.

Работа с графикой в C#: простой пример
Содержание
  1. Какие есть способы рисовать в C#
  2. Основы GDI+: Graphics, Pen и Brush
  3. Пример 1: рисунок в окне Windows Forms
  4. Пример 2: рисование на Bitmap и сохранение файла
  5. Пример 3: анимация на таймере
  6. Градиенты, текстуры и текст
  7. Рисование мышью
  8. Графика в WPF
  9. Распространённые ошибки
  10. Частые вопросы

Какие есть способы рисовать в C#

Выбор библиотеки зависит от платформы и задачи. Для учебных примеров и небольших утилит под Windows проще всего System.Drawing, а для кроссплатформенных проектов и серверной обработки изображений берут сторонние библиотеки.

ТехнологияПлатформаПодходит для
System.Drawing (GDI+)WindowsПростые рисунки в Windows Forms, диаграммы, обработка картинок
WPFWindowsВекторная графика, анимация, привязка данных, 3D
SkiaSharpWindows, Linux, macOS, мобильныеБыстрое 2D, генерация изображений на сервере
ImageSharpЛюбаяИзменение размера, фильтры, конвертация форматов
Unity, MonoGameРазныеИгры и интерактивная графика

Важная оговорка: начиная с .NET 6 пакет System.Drawing.Common официально работает только в Windows. Если нужен код, который запускается и в Linux-контейнере, лучше сразу выбрать SkiaSharp или ImageSharp.

Основы GDI+: Graphics, Pen и Brush

Любое рисование строится вокруг трёх объектов. Graphics - поверхность, на которую рисуют, будь то форма, элемент управления или картинка в памяти. Pen (перо) определяет цвет и толщину линий для контуров. Brush (кисть) закрашивает внутренности фигур. Системные перья и кисти вроде Pens.Black и Brushes.Red удобны для коротких примеров, а собственные нужно освобождать через using.

Метод GraphicsЧто рисует
DrawLineЛинию между двумя точками
DrawRectangle, FillRectangleКонтур и заливка прямоугольника
DrawEllipse, FillEllipseЭллипс или круг
DrawPolygon, FillPolygonМногоугольник по массиву точек
DrawArc, DrawPieДуга и сектор
DrawStringТекст
DrawImageГотовое изображение
ClearЗаливка всей поверхности цветом

Пример 1: рисунок в окне Windows Forms

В Windows Forms рисовать нужно в обработчике события Paint. Система сама вызывает его, когда окно открывается, сворачивается или перекрывается другим. Если рисовать где-то ещё, изображение исчезнет при первой перерисовке.

public class MainForm : Form
{
    public MainForm()
    {
        Text = "Простая графика";
        ClientSize = new Size(420, 300);
        DoubleBuffered = true;   // убирает мерцание
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);
        var g = e.Graphics;
        g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

        // оси и линия
        g.DrawLine(Pens.Gray, 20, 260, 400, 260);
        g.DrawLine(Pens.Gray, 20, 260, 20, 20);

        // заливка и контур
        g.FillRectangle(Brushes.LightSkyBlue, 60, 120, 100, 140);
        g.DrawRectangle(Pens.SteelBlue, 60, 120, 100, 140);

        using var redPen = new Pen(Color.Crimson, 3);
        g.DrawEllipse(redPen, 200, 60, 140, 140);

        // многоугольник
        Point[] triangle = { new(230, 240), new(300, 150), new(370, 240) };
        g.FillPolygon(Brushes.Orange, triangle);

        // текст
        using var font = new Font("Segoe UI", 14, FontStyle.Bold);
        g.DrawString("Фигуры в GDI+", font, Brushes.DarkSlateGray, 20, 10);
    }

    [STAThread]
    static void Main()
    {
        ApplicationConfiguration.Initialize();
        Application.Run(new MainForm());
    }
}

Код рассчитан на проект Windows Forms под .NET 8, где неявные using уже подключают нужные пространства имён. Свойство SmoothingMode включает сглаживание: без него наклонные линии и окружности выглядят зубчатыми. Установка DoubleBuffered = true заставляет форму рисовать в буфер и выводить готовый кадр целиком, поэтому изображение не мигает.

Пример 2: рисование на Bitmap и сохранение файла

Когда нужно сгенерировать картинку, например превью, график или водяной знак, окно не требуется. Создают Bitmap, получают из него Graphics, рисуют и сохраняют результат. Для консольного проекта сначала добавьте пакет System.Drawing.Common командой dotnet add package System.Drawing.Common.

using System.Drawing;
using System.Drawing.Imaging;

using var bmp = new Bitmap(300, 200);
using (var g = Graphics.FromImage(bmp))
{
    g.Clear(Color.White);
    g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

    g.FillEllipse(Brushes.Gold, 20, 20, 160, 160);
    g.DrawEllipse(new Pen(Color.Black, 2), 20, 20, 160, 160);

    using var font = new Font("Arial", 18, FontStyle.Bold);
    g.DrawString("Привет, C#", font, Brushes.Navy, 130, 80);
}

bmp.Save("circle.png", ImageFormat.Png);
Console.WriteLine("Файл сохранён: " + Path.GetFullPath("circle.png"));

Формат выбирается вторым аргументом: Png подходит для рисунков с чёткими линиями, Jpeg для фотографий. Если не передать формат, файл может сохраниться в BMP независимо от расширения в имени.

Пример 3: анимация на таймере

Чтобы нарисованное двигалось, нужно менять координаты и просить форму перерисоваться. Для этого подойдёт System.Windows.Forms.Timer, который срабатывает в потоке интерфейса, а перерисовку запрашивают методом Invalidate. Сам рисунок всегда выполняется в OnPaint.

public class BallForm : Form
{
    private float _x = 40, _y = 40, _dx = 3, _dy = 2.5f;
    private const int Radius = 20;
    private readonly System.Windows.Forms.Timer _timer = new() { Interval = 16 };

    public BallForm()
    {
        ClientSize = new Size(480, 320);
        DoubleBuffered = true;
        _timer.Tick += (s, e) =>
        {
            _x += _dx; _y += _dy;
            if (_x < 0 || _x + Radius * 2 > ClientSize.Width)  _dx = -_dx;
            if (_y < 0 || _y + Radius * 2 > ClientSize.Height) _dy = -_dy;
            Invalidate();
        };
        _timer.Start();
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
        e.Graphics.FillEllipse(Brushes.Tomato, _x, _y, Radius * 2, Radius * 2);
    }
}

Интервал в 16 миллисекунд даёт около 60 кадров в секунду. Но точность таймера Windows Forms невысока, поэтому для серьёзных игр лучше брать игровые движки. Шаг движения стоит привязывать ко времени, а не к числу тиков, если скорость должна быть одинаковой на любом компьютере.

Градиенты, текстуры и текст

Однотонная заливка быстро надоедает. Класс LinearGradientBrush создаёт плавный переход между цветами, а TextureBrush заполняет фигуру повторяющимся изображением. Подробно о градиентах рассказано в материале градиент в C# через GDI+.

using System.Drawing.Drawing2D;

var rect = new Rectangle(20, 20, 300, 80);
using var brush = new LinearGradientBrush(rect, Color.DeepSkyBlue, Color.MediumPurple, 0f);
e.Graphics.FillRectangle(brush, rect);

using var sf = new StringFormat
{
    Alignment = StringAlignment.Center,
    LineAlignment = StringAlignment.Center
};
e.Graphics.DrawString("Градиент", new Font("Segoe UI", 20), Brushes.White, rect, sf);

Класс StringFormat помогает выровнять текст внутри прямоугольника по центру, не вычисляя координаты вручную. Это пригодится для подписей на кнопках и диаграммах.

Рисование мышью

Интерактивные примеры оживляют программу. Чтобы пользователь мог рисовать мышью, сохраняют точки в список и рисуют линии между ними в OnPaint. Так сохраняется рисунок при перерисовке окна.

public class PaintForm : Form
{
    private readonly List<Point> _points = new();
    private bool _drawing;

    public PaintForm() { DoubleBuffered = true; ClientSize = new Size(500, 350); }

    protected override void OnMouseDown(MouseEventArgs e) { _drawing = true; _points.Add(e.Location); }
    protected override void OnMouseUp(MouseEventArgs e) { _drawing = false; _points.Add(Point.Empty); }

    protected override void OnMouseMove(MouseEventArgs e)
    {
        if (!_drawing) return;
        _points.Add(e.Location);
        Invalidate();
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        using var pen = new Pen(Color.Black, 3) { StartCap = System.Drawing.Drawing2D.LineCap.Round, EndCap = System.Drawing.Drawing2D.LineCap.Round };
        for (int i = 1; i < _points.Count; i++)
        {
            if (_points[i] == Point.Empty || _points[i - 1] == Point.Empty) continue;
            e.Graphics.DrawLine(pen, _points[i - 1], _points[i]);
        }
    }
}

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

Графика в WPF

WPF подходит к графике иначе: фигуры описываются декларативно в XAML как элементы интерфейса, а система сама их перерисовывает. Масштабирование вектора происходит без потери качества. С основами окон WPF знакомит статья о разработке оконных приложений на C#.

<Canvas Width="300" Height="200" Background="White">
    <Ellipse Width="120" Height="120" Canvas.Left="20" Canvas.Top="20"
             Fill="Gold" Stroke="Black" StrokeThickness="2" />
    <Rectangle Width="100" Height="60" Canvas.Left="170" Canvas.Top="60"
               Fill="SteelBlue" RadiusX="8" RadiusY="8" />
    <TextBlock Text="Привет, WPF" Canvas.Left="30" Canvas.Top="160" FontSize="18" />
</Canvas>

Для WPF не нужен System.Drawing: в нём свои классы Brush, Pen и Geometry из пространства имён System.Windows.Media. Старайтесь не смешивать их с одноимёнными классами GDI+ в одном файле.

Распространённые ошибки

  • Рисование вне события Paint: изображение пропадает после сворачивания окна.
  • Не освобождают перья, кисти и шрифты, созданные через new, из-за чего растёт число дескрипторов GDI.
  • Мерцание при анимации: не включили DoubleBuffered.
  • Запуск кода с System.Drawing на Linux или в macOS под .NET 6 и новее приводит к PlatformNotSupportedException.
  • Ожидание, что круг задаётся центром и радиусом: методы DrawEllipse принимают левый верхний угол и размеры описанного прямоугольника.
  • Сохранение Bitmap в файл, который всё ещё открыт через Image.FromFile: файл блокируется, нужно загружать копию.

Частые вопросы

Как нарисовать круг в C#?

В обработчике Paint вызовите g.DrawEllipse(pen, x, y, width, height) с равными шириной и высотой. Для заливки используйте FillEllipse с кистью.

Где рисовать в Windows Forms: в Paint или сразу в форме?

Только в событии Paint или переопределённом OnPaint. Рисунок, выполненный через CreateGraphics, исчезает при перерисовке окна.

Почему System.Drawing не работает на Linux?

С .NET 6 пакет System.Drawing.Common поддерживается только в Windows. Для Linux и macOS используйте SkiaSharp или ImageSharp.

Как убрать мерцание при перерисовке?

Включите DoubleBuffered = true у формы или элемента и вызывайте Invalidate только для изменившейся области.

Как сохранить нарисованное в картинку?

Рисуйте на объекте Bitmap через Graphics.FromImage и вызовите bmp.Save("файл.png", ImageFormat.Png). Для окна можно скопировать его содержимое методом DrawToBitmap.

Что выбрать для игр на C#?

Для простых 2D-игр подойдёт MonoGame, для 3D и мобильных проектов Unity. GDI+ и таймеры Windows Forms годятся только для учебных прототипов.