WPF графики, диаграммы

Все исходники /  Язык программирования C# /  OS Windows /  Desktop /  WPF программирование / WPF графики, диаграммы

Программа WPF Charts

Программа построения графиков

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

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

WPF drawing

Все графики построены с применением графической группы элементов WPF пространства имен System.Windows.Shapes. Это пространство включает как готовые фигуры, так и возможность создания произвольной геометрии любой сложности. System.Windows.Shapes включает в себя:
  • Line - простейшая геометрическая фигура образуемая двумя точками.
  • Ellipse - создание окружности, круга или эллипса.
  • Rectangle - прямоугольники и квадраты.
  • Polyline - создание ломанной линии. Построение основано на массиве точек. Конечная точка предыдущего сегмента является началом следующего сегмента-линии.
  • Polygon - разновидность ломанной линии, у которой начальный и конечный сегмент-линия замыкаются.
  • Path - составная графика из фигур и последовательности геометрических примитивов, их комбинаций и объединений. Даёт возможность построения графической фигуры любой сложности. Параметры фона и окантовочной черты действует на Path целиком. Для создания нескольких сложных фигур разного цвета необходимо создавать отдельный Path.

Абстрактный класс Chart

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

В классе Chart определяются размеры графиков и рисуется сетчатый фон. Фон создан кистью DrawingBrush. Данная кисть примечательна тем, что фон заполняется тем, что вы нарисуете программным кодом. Нарисовали квадрат, квадратом заполните фон, нарисовали эллипс - фон заполнится кружочками и т.д. Доступна для рисования и более разнообразная геометрия, состоящая из множества простейших фигур. Если не указывать размер области просмотра Viewport кисть заполнит фон элемента одной растянутой фигурой. Если же размеры области просмотра сделать меньше размера элемента возможна плиточная мозаика из нарисованной фигуры. Размер области просмотра можно задавать относительными и абсолютными величинами.

Листинг кода создания сетчатой кисти для заднего фона графиков.
private Brush DrawLines(double actualwidth, double widthchart, double padding)
{
    double W = actualwidth;
    double w = widthchart;
    double offset = padding;

    // --- Величины для относительных расчетов ---

    // W - (абсолютная величина как общий знаменатель)
    // (Относительная ширина контейнера графика (рисуется сетка заднего фона)) = 1 (W/W)

    // x - (Относительная ширина поля графика) = w/W
    double x = w / W;

    // delta - (Относительное смещение сетки заднего фона графика) = offset/W
    double delta = offset / W;

    // --- 

    // Количество линий по горизонтали и вертикали.
    // По вертикали всего 15 линий, но график только до 10-ой.
    int numLines = 10;

    DrawingBrush brush = new()
    {
        // Режим задающий правило заполнения фона элемента плитками 
        TileMode = TileMode.Tile,

        // Область просмотра задана относительными величинами.
        // График будет иметь в высоту и в ширину одинаковое кол-во линий.
        Viewport = new Rect(delta, 0, x / numLines, _factor / numLines),

        // Рисуем прямоугольник, формирующий фоновую сетку.
        Drawing = new GeometryDrawing()
        {
            Pen = new(Brushes.Black, 0.05),
            Brush = new SolidColorBrush(Color.FromRgb(240, 240, 240)),
            Geometry = new RectangleGeometry(new Rect(0, 0, 45, 20))
        }
    };

    return brush;
}

Bar Chart – столбиковая диаграмма

Столбиковая диаграмма

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

В график BarChat возможно добавление любых значений, например могут быть значения от 1 до 5000. И поскольку высота окна ограничена, внутренняя логика класса, при добавлении нового значения, заново определяет максимальную высоту прямоугольников. Благодаря этому столбцы графика сохраняют визуальные пропорции значений и, в тоже время, габариты баров всегда остаются в пределах своего поля.

Алгоритм вычисления максимальной высоты прямоугольников при добавлении нового значения:
  1. Суммируются все значения, включая новое добавленное.
  2. Итоговая сумма значений делится на количество значений в графике. Получаем общий знаменатель для всех значений.
  3. Для каждого значения высчитывается высота по формуле:
    Высота = значение / общий знаменатель
Кроме высоты, при добавлении нового значения должна корректироваться и ширина баров, иначе они могут выйти за пределы поля графика. Алгоритм вычисления ширины баров:
  1. Вычисляется количество значений.
  2. Далее ширина бара вычисляется по формуле:
    Ширина бара = (ширина поля графика – ширину промежутков между барами * (кол-во значений -1)) / кол-во значений
Листинг метода добавления нового значения в график:
public override void AddValue(double value)
{
    // Получаем все значения которые уже есть в графике.
    List listValues = ChartBackground.Children.OfType().Select(p => (double)p.Tag).ToList();

    // Добавляем новое значение в график.
    listValues.Add(value);

    // Вычисляем новую ширину бара, чтобы график поместился 
    // полностью на ширину поля.
    double widthBar = (WidthChart - ((listValues.Count - 1) * _gap)) / listValues.Count;

    // Для ограничения высоты графика, вне зависимости от абсолютных значений,
    // вычислим общий знаменатель. И самое большое значение будет на максимальной
    // допустимой высоте, остальные пропорционально ниже.
    double maxValue = listValues.Max();
    double denominator = maxValue / HeightChart;

    // Удалим текущие элементы графика.
    Clear();

    foreach (double val in listValues)
    {
        int count = ChartBackground.Children.OfType().Count();

        // Относительная высота точки от нижнего края.
        // Для этого все абсолютные значения делятся на общий знаменатель,
        // чтобы максимальная высота точек не выходила выше установленной.
        double heightPoint = val / denominator;

        // Для улучшения визуального восприятия.
        if (heightPoint < 3)
        {
            heightPoint = 3;
        }

        // Координата X расположения полосы, координата Y равна 0:
        // полоса начинается от нижнего края.
        double x = (count * (widthBar + _gap)) + (ChartBackground.ActualWidth - WidthChart) / 2;

        // Создание полосы.
        Rectangle bar = CreateBar(x, heightPoint, widthBar, val);
        _ = ChartBackground.Children.Add(bar);

        // Надпись над полосой.
        Label title = CreateTitle(x, bar.Height, widthBar, val);
        _ = ChartBackground.Children.Add(title);
    }
}

Line Chart – линейный график

Линейный график

Линейный график строится на базе ломаной линии рисуемый классом PolyLine. Ломаная линия упрощает построение графика, поскольку для рисования следующей линии требуется только одна точка. Если использовать множество одиночных линий класса Line, потребуется в два раза больше переменных для хранения координат.

Узловые точки рисуются фигурами класса Ellipse. Благодаря узловым точкам повышается наглядность линейного графика.

Линейный график также принимает любые значения. Коррекция высоты и ширины графика аналогична алгоритму столбикового графика BarsChart. Над узловыми точками выводится текст значений. Координаты узловых точек являются базовыми для вывода текста рядом с узловой точкой.

Реализация метода вычисления размеров линейного графика при добавлении нового значения:
public override void AddValue(double value)
{
    // Получаем все значения которые уже есть в графике.
    List listValues = ChartBackground.Children.OfType().Select(p => (double)p.Tag).ToList();

    // Вычисляем новую длину отрезка ломаной линии, чтобы график поместился 
    // полностью на ширину поля.
    double lengthSectionLine = listValues.Count > 0 ? WidthChart / listValues.Count : WidthChart;

    // Добавляем новое значение в график.
    listValues.Add(value);

    // Для ограничения высоты графика, вне зависимости от абсолютных значений,
    // вычислим общий знаменатель. И самое большое значение будет на максимальной
    // допустимой высоте. остальные пропорционально ниже.
    double maxValue = listValues.Max();
    double denominator = maxValue / HeightChart;

    // Удалим текущие элементы графика.
    Clear();

    // Инициализация новой ломаной линии.
    Polyline _polyline = new();
    _polyline.Stroke = Brushes.BlueViolet;
    _polyline.StrokeThickness = _lineThickness;
    _polyline.StrokeDashCap = PenLineCap.Flat;
    _polyline.StrokeLineJoin = PenLineJoin.Round;
    _polyline.HorizontalAlignment = HorizontalAlignment.Left;
    ChartBackground.Children.Add(_polyline);


    // Создание графика по текущим абсолютным значениям.
    // Абсолютные значения сохраняются в свойствах Ellipse.Tag
    foreach (double val in listValues)
    {
        // Счётчик добавленных в график узловых точек.
        int count = ChartBackground.Children.OfType().Count();

        // Относительная высота точки от нижнего края.
        // Для этого все абсолютные значения делятся на общий знаменатель,
        // чтобы максимальная высота точек не выходила выше установленной.
        double heightPoint = val / denominator;

        // Координаты узловой точки.
        double x = (count * lengthSectionLine) + (ChartBackground.ActualWidth - WidthChart) / 2;
        double y = heightPoint;

        // Узловая точка графика.
        Ellipse point = CreatePoint(x, y, _sizePoint, val);
        ChartBackground.Children.Add(point);

        // Надпись около узловой точки.
        Label title = CreateTitle(x - (_sizePoint / 2), y, val);
        ChartBackground.Children.Add(title);

        // Отрезок линии соединяющий предыдущую и текущую узловую точку.
        _polyline.Points.Add(new Point(x, ChartBackground.ActualHeight - y /* переворачиваем значение: отсчёт идёт от bottom*/));
    }
}

Pie Chart – круговая диаграмма

Круговая диаграмма

Круговая диаграмма даёт наглядное представление о доли каждого значения в общем объёме.

Круговая диаграмма построена на классе Path, обладающим большими возможностями Но вместе с большими возможностями, инициализация графики с помощью данного класса сложнее и содержит значительно больше кода, чем рисование, например, с помощью Ellipse, Rectangle, Line и т.п.

Каждый сектор круга диаграммы окрашен в отдельный цвет. Чтобы обеспечить разнообразие цветов для каждого сектора используется отдельный экземпляр Path. Геометрия сектора построена на фрагментах, которые образуют фигуру, входящую в состав Path.

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

Листинг метода создания сектора диаграммы:
/// <summary>
/// Создание сектора диска с собственным цветом.
/// </summary>
/// <param name="degree">угол сектора в градусах</param>
/// <param name="offset">угол смещение на величину угла предыдущего сектора</param>
/// <param name="value">абсолютное значение пункта графика</param>
/// <returns></returns>
private Path CreateSector(double degree, double offset, double value)
{
    Random random = new();

    Path path = new()
    {
        StrokeThickness = 5,
        Stroke = Brushes.White,
        Fill = new SolidColorBrush(Color.FromArgb(255, 
                                                   (byte)random.Next(50, 256), 
                                                   (byte)random.Next(50, 256), 
                                                   (byte)random.Next(50, 256))),

        Data = new PathGeometry()
        {
            Figures = new PathFigureCollection()
            {
                SectorGeometry(degree, offset)
            }
        },

        // Каждый Path будет хранить свои координаты.
        Tag = new StoredValues()
        {
            Degree = degree,
            Offset = offset,
            Value = value
        }
    };

    return path;
}
Пояснительный программный код вычисления углового смещения сектора:
// Это важные пояснительные строчки:
// как формируется смещение в градусах.
Path path = CreatePath(45, 0, Brushes.Red);
path.Tag = value;
_canvas.Children.Add(path);

path = CreatePath(15, 45, Brushes.Blue);
_canvas.Children.Add(path);

path = CreatePath(120, 60, Brushes.GreenYellow);
_canvas.Children.Add(path);

path = CreatePath(225, 180, Brushes.GreenYellow);
_canvas.Children.Add(path);

Исходный код программы WPF Charts

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

Исходный код написан на языке C#, в интегрированной среде MS Visual Studio 2019, framework NET5.

Скачать исходник