Как нарисовать график
- Формируем интерфейс
- Добавляем обработчик события Paint
- Рисуем график
- Сдвигаем график в центр области
- Переворачиваем график
- Масштабируем график
- Рисуем сетку
- Решаем проблему перерисовки при изменении размеров формы
- Сглаживаем график
- Подключаем учет коэффициентов
- Итоговый код
При разработке более сложных приложений, в процессе обработки данных получаются результаты которые содержат много числовых данных. И хотя выводить эти данные в виде строк из циферок можно, но оценивать полученный результат в таком виде не очень удобно.
Поэтому человечество придумало выводить такие данные в виде графиков. Которые в свою очередь эволюционировало в инфографику.
Но для инфографики нам неплохо было бы еще и на художественном поучиться, а у нас времени даже не программирование не очень много получается, так что мы будем просто рисовать график.
Рисовать будем простую штуку, функцию вида
где коэффициенты k, n можно будет поменять.
Формируем интерфейс
Добавим:
-
TextBox под коэффициент k, и соответствующий Label, свойства установим
- (Name): txtK
- Text: 1
-
TextBox под коэффициент n, и соответствующий Label, свойства установим
- (Name): txtN
- Text: 2 – чтоб парабола получилась
-
PictureBox, на нем будем рисовать график
- (Name): pictureBox
- BackColor: White – чтобы область под график выглядела белой
Получим
Чтобы область под график pictureBox изменяла свои размеры с изменением размеров формы, установим ей свойство Anchor как на картинке:
теперь если менять размеры формы, и область под график будет меняться соответственно:
Добавляем обработчик события Paint
Всю отрисовку на PictureBox, полагается выполнять внутри функции привязанного к событию Paint
Выбираем на форме pictureBox переходим в список событий, и кликаем дважды на свойство Paint
оказываемся в новосозданной функции
namespace GraphicApp
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void pictureBox_Paint(object sender, PaintEventArgs e)
{
// ТОЛЬКО ЧТО СОЗДАННЫЙ ХЭНДЛЕР
}
}
}
Рисуем график
Попробуем нарисовать график, пока без учета коэффициентов.
Чтобы нарисовать график нам надо сформировать список точек, подготовим его
private void pictureBox_Paint(object sender, PaintEventArgs e)
{
// для отрисовки придется использовать тип данных PointF,
// который представляет собой точку в пространстве,
// с двумя координатами X и Y, поэтому и список инициализируем
// под хранение этого типа
var points = new List<PointF>();
// формируем точки на промежутке [-30; 30]
for(var x=-30;x<=30;++x)
{
// добавляем очередную точку с y = x * x
points.Add(new PointF(x, x * x));
}
}
и так, точки у нас есть, теперь их надо нарисовать. Прежде чем вызвать функцию для отрисовки, надо указать как будет выглядеть линия, Для этого надо создать объект типа Pen (то бишь перо, ручка), у которого есть два основных параметра: цвет и ширина линии, создадим такой объект:
private void pictureBox_Paint(object sender, PaintEventArgs e)
{
var points = new List<PointF>();
for(var x=-30;x<=30;++x)
{
points.Add(new PointF(x, x * x));
}
// если смотреть на это более высокоуровнево
// тут мы, типа, создали ручку, которой сейчас будем рисовать
var blackPen = new Pen(Color.Black, 1);
}
ну а теперь можно нарисовать график, для этого воспользуемся функций DrawLines, объекта Graphics (так называемый графический контекст устройства) который привязан к аргументу PaintEventArgs e
private void pictureBox_Paint(object sender, PaintEventArgs e)
{
var points = new List<PointF>();
for(var x=-30;x<=30;++x)
{
points.Add(new PointF(x, x * x));
}
var blackPen = new Pen(Color.Black, 1);
e.Graphics.DrawLines(blackPen, points.ToArray());
}
Увидим что-то несуразное:
Если попытаться угадать в этом параболу, то увидим сразу несколько проблем:
- видно только правую ветвь параболы
- парабола перевёрнута
- график сильно маленький
Сдвигаем график в центр области
Решить все перечисленные выше проблемы можно используя матрицы перехода https://ru.wikipedia.org/wiki/Матрица_перехода
Матрицы перехода являются одним из основополагающих математических инструментов в создании 3D графики, который в упрощённом виде работает и в 2D графике (с чем мы собственно сейчас и работаем).
Ко всякому объекту типа Graphics привязана матрица переходов (доступная через свойство Transform). По умолчанию она представляет собой единичную матрицу. Мы можем изменять матрицу используя методы объекта типа Graphics
- TranslateTransform – для перемещения центра координат
- ScaleTransform – для масштабирования
- RotateTransform – для поворота вокруг центра координат
Применяя эти методы в разных порядках можно перемещать, масштабировать и крутить объекты и даже группы объектов малой кровью.
Мы конечно особо ничего крутить ничего не планируем. Но давайте перенесем центр координат в центр pictureBox, добавим строчку:
private void pictureBox_Paint(object sender, PaintEventArgs e)
{
// ...
// добавили вызов метода TranslateTransform, у которого два параметра: сдвиг по оси x и y
e.Graphics.TranslateTransform(pictureBox.Width / 2, pictureBox.Height / 2);
e.Graphics.DrawLines(blackPen, points.ToArray());
}
получим:
Переворачиваем график
Очевидно, что у параболы с коэффициентом 1, ветви параболы должны быть направлены вверх. И хотя мы абсолютно верно формируем список точек:
private void pictureBox_Paint(object sender, PaintEventArgs e)
{
// ...
for(var x=-30;x<=30;++x)
{
points.Add(new PointF(x, x * x));
}
// ...
}
ошибка возникает из-за того, что центр координат, у большинства системных объектов, находится в левом верхнем углу:
чтобы перевернуть график воспользуемся функцией ScaleTransform
private void pictureBox_Paint(object sender, PaintEventArgs e)
{
// ...
e.Graphics.TranslateTransform(pictureBox.Width / 2, pictureBox.Height / 2);
// у функции два параметра, скалирование по x и скалирование по y
// использую 1 для скалирования по x, мы никак не меняем масштаб по x
// а вот используя -1 для скалирования по y, мы как раз добивается эффекта переворота
e.Graphics.ScaleTransform(1, -1);
e.Graphics.DrawLines(blackPen, points.ToArray());
}
красота
Масштабируем график
И вот вроде все отлично, но график рисуется в пиксельной системе координат. Что на небольших мониторах выглядит еще куда ни шло, но на современных Ultra HD и 4K придется использовать лупу. Мы конечно не хотим заставлять пользователя доставать лупу, но зато мы можем увеличить масштаб графика, снова воспользовавшись функций ScaleTransform. Добавим строчку:
private void pictureBox_Paint(object sender, PaintEventArgs e)
{
// ...
e.Graphics.TranslateTransform(pictureBox.Width / 2, pictureBox.Height / 2);
e.Graphics.ScaleTransform(1, -1);
// увеличиваем масштаб
e.Graphics.ScaleTransform(10, 10)
e.Graphics.DrawLines(blackPen, points.ToArray());
}
проверяем:
неплохо, но линия какая-та толстая. Она масштабируется вместе со всем остальным, что не есть хорошо. Чтобы избавится от масштабирования линии придется модифицировать ее матрицу перехода. Да-да, у объекта типа Pen тоже есть своя матрица перехода, также доступная через свойство Transform.
Какую же матрицу перехода применять к линии? Очевидно, обратную к матрице основного Graphics. Сделаем это:
private void pictureBox_Paint(object sender, PaintEventArgs e)
{
// ...
e.Graphics.ScaleTransform(10, 10)
// придется склонировать матрицу перехода e.Graphics, используя метода Clone
// а клонировали, так как все объекты в C# ссылочные,
// а метод Invert, изменяет матрицу прям внутри объекта, у которой он был вызван
// и вот чтобы случайно не изменить матрицу перехода e.Graphics
// мы создаем копию исходной матрицы
var penTransform = e.Graphics.Transform.Clone();
penTransform.Invert(); // обращаем матрицу penTransform
// фиксируем матрицу перехода у пера,
// как обратную к матрице перехода e.Graphics
blackPen.Transform = penTransform;
e.Graphics.DrawLines(blackPen, points.ToArray());
}
вот теперь другое дело
Рисуем сетку
Чтобы лучше ориентироваться где какая точка находится нарисуем сетку с размером ячейки в одну единицу
blackPen.Transform = penTransform;
// добавляем серое перо
var grayPen = new Pen(Color.LightGray, 1);
grayPen.Transform = penTransform; // матрица перехода та же, что и у blackPen
for (var x = -10; x <= 10; ++x) // рисуем сетку 10x10
{
var pen = x == 0 ? blackPen : grayPen; // чтобы центральные оси рисовались черным пером
e.Graphics.DrawLine(pen, x, -10, x, 10);
e.Graphics.DrawLine(pen, -10, x, 10, x);
}
e.Graphics.DrawLines(blackPen, points.ToArray());
}
проверяем:
кстати можно сделать чтобы единица на экране соответствовала одному сантиметру в реальной жизни и получить своего рода экранную линейку. Для этого надо воспользоваться свойством DpiX и DpiY объекта Graphics, пробуем. Dpi определяет количество точек на дюйм. В одном дюйме примерно 2.54 сантиметра, следовательно, нам надо отредактировать вызов функции скалирования следующим образом:
private void pictureBox_Paint(object sender, PaintEventArgs e)
{
// ...
e.Graphics.ScaleTransform(1, -1);
e.Graphics.ScaleTransform(e.Graphics.DpiX / 2.54f, e.Graphics.DpiY / 2.54f); // <<< МЕНЯЕМ ЭТУ СТРОЧКУ
var penTransform = e.Graphics.Transform.Clone();
// ...
}
достаем линейку, прикладываем к экрану:
так себе точность, конечно, получается, но результат все-таки радует.
Решаем проблему перерисовки при изменении размеров формы
Возможны вы уже пробовали менять размеры формы, и заметили, что получаемый результат далек от ожидаемого.
связано такое поведение с особенностью перерисовки объектов на форме. Перерисовывается только изменённая часть. А так как при разных размерах формы положение графика и сетки разное получается наложение десятка разных изображений, что приводит к таким ужасным артефактам.
Чтобы при изменении размера формы изображение перерисовывалось всегда целиком, добавьте обработчик событию Resize, объекта pictureBox
и в обработчике вставьте строчку:
private void pictureBox_Resize(object sender, EventArgs e)
{
// строчка обозначает что при изменении размеров формы
// вся область pictureBox стоит признать невалидной
// и затребовать полную перерисовку объекта
pictureBox.Invalidate();
}
проверяем:
Сглаживаем график
Сейчас у нас графи рисуется по целым координатам, из-за чего выглядит слегка уродливо и сильно угловато. Давайте увеличим количество точек. В этот раз, при формировании списка точек, я, вместо цикла, воспользуюсь Linq функциями, которые позволят мне сформировать список точек в функциональной манере с использованием лямбда-выражений.
private void pictureBox_Paint(object sender, PaintEventArgs e)
{
/*
!!! этот код убираем
var points = new List<PointF>();
for(var x=-10;x<=10;++x)
{
points.Add(new PointF(x, x * x));
}*/
var count = 100; // хочу сто точек
var step = 0.1f; // шаг 0.1
var points = Enumerable.Range(0, count) // хитрый метод, вернет массив чисел от 0 до 99
.Select(x => step * x - step * count / 2) // пересчитаю x, получится промежуток [-5;5) с шагом 0.1
.Select(x => new PointF(x, x * x)); // считаю y, и формирую массив точек в декартовой системе координат
var blackPen = new Pen(Color.Black, 1);
// ....
}
запускаем, получаем гладенький график:
Подключаем учет коэффициентов
Играясь с графиками совсем забыли про коэффициенты. Посчитаем их значения и добавим в учет при формировании точек:
private void pictureBox_Paint(object sender, PaintEventArgs e)
{
float k, power;
try
{
k = float.Parse(txtK.Text);
power = float.Parse(txtN.Text);
}
catch (FormatException)
{
return;
}
var count = 100;
var step = 0.1f;
var points = Enumerable.Range(0, count)
.Select(x => step * x - step * count / 2)
.Select(x => new PointF(x, k * (float) Math.Pow(x, power)));
// ....
}
запускаем:
хм, чего-то не работает…
А ну да, нам же надо чтобы при изменении значения, вызывался метод pictureBox.Invalidate(), тот самый которые отправляет запрос на перерисовку всего pictureBox. Переключаемся на форму, и кликаем два разу на txtK, а затем на txtN, добавляем код в соответствующие обработчики:
private void txtK_TextChanged(object sender, EventArgs e)
{
pictureBox.Invalidate();
}
private void txtN_TextChanged(object sender, EventArgs e)
{
pictureBox.Invalidate();
}
проверяем:
Итоговый код
Если вы в какой-то момент запутались, то вот вам итоговый код функции отрисовки:
private void pictureBox_Paint(object sender, PaintEventArgs e)
{
float k, power;
try
{
k = float.Parse(txtK.Text);
power = float.Parse(txtN.Text);
}
catch (FormatException)
{
return;
}
var count = 100;
var step = 0.1f;
var points = Enumerable.Range(0, count)
.Select(x => step * x - step * count / 2)
.Select(x => new PointF(x, k * (float)Math.Pow(x, power)));
var blackPen = new Pen(Color.Black, 1);
e.Graphics.TranslateTransform(pictureBox.Width / 2, pictureBox.Height / 2);
e.Graphics.ScaleTransform(1, -1);
e.Graphics.ScaleTransform(e.Graphics.DpiX / 2.54f, e.Graphics.DpiY / 2.54f);
var penTransform = e.Graphics.Transform.Clone();
penTransform.Invert();
blackPen.Transform = penTransform;
var grayPen = new Pen(Color.LightGray, 1);
grayPen.Transform = penTransform;
for (var x = -10; x <= 10; ++x)
{
var pen = x == 0 ? blackPen : grayPen; //
e.Graphics.DrawLine(pen, x, -10, x, 10);
e.Graphics.DrawLine(pen, -10, x, 10, x);
}
e.Graphics.DrawLines(blackPen, points.ToArray());
}
Конец.