Построение графиков на WPF (.Net 3.0)

[ratings] Недавно возникла необходимость построить пару графиков. Это были результаты расчета некой физической модели. И я решил, что было бы удобно построить их прямо в рассчитывающей программе с помощью WPF (.Net 3.0). Хотя задача довольно проста, можно долго копаться в хелпе, ища как сделать ту или иную вещь. Поэтому приведу тут исчерпывающий пример, который ответит на многие вопросы.

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

график

 

Для начала подготовим данные. Пусть это будут просто массивы синусов и косинусов:

//Сгенерируем данные для графиков
int Np = 30;
double[] Data1 = new double[Np + 1];
double[] Data2 = new double[Np + 1];

for (int i = 0; i < Np + 1; i++)
{
  Data1[i] = Math.Sin(i / 5.0) + 1;
  Data2[i] = Math.Cos(i / 5.0) + 1;
}

Теперь собственно построение.
Вначале создадим группу геометрических изображений DrawingGroup, каждое из которых будет нарисовано своими кистями.
Я разбил процесс построения графика на стадии. Этому соответствует цикл по DrawingStage. На каждой стадии создается объект класса GeometryDrawing, который впоследствии добавляется в DrawingGroup. Внутри GeometryDrawing создается группа геометрических примитивов GeometryGroup. Наконец GeometryGroup может содержать в себе объекты классов LineGeometry, RectangleGeometry, etc.

DrawingGroup aDrawingGroup = new DrawingGroup();

for (int DrawingStage = 0; DrawingStage < 10; DrawingStage++)
{
  GeometryDrawing drw = new GeometryDrawing();
  GeometryGroup gg = new GeometryGroup();

  //Задный фон
  if (DrawingStage == 1)
 {
    drw.Brush = Brushes.Beige;
    drw.Pen = new Pen(Brushes.LightGray, 0.01);

    RectangleGeometry myRectGeometry = new RectangleGeometry();
    myRectGeometry.Rect = new Rect(0, 0, 1, 1);
    gg.Children.Add(myRectGeometry);
  }

  //тут остальные стадии.....

  drw.Geometry = gg;
  aDrawingGroup.Children.Add(drw);
}

image1.Source = new DrawingImage(aDrawingGroup);

На первой стадии нарисуем задний фон, который представляет из себя квадрат бежевого цвета с серыми краями. Задаются цвета и стили кистей (Brush, Pen) у класса GeometryDrawing. Обратите внимание на координаты. Фактически их можно задавать любыми. Здесь я использую зону чуть шире чем квадрат 0..1×0..1. На второй стадии нарисуем мелкую сетку на фоне:

//Мелкая сетка
if (DrawingStage == 2)
{
  drw.Brush = Brushes.Beige;
  drw.Pen = new Pen(Brushes.Gray, 0.003);
  

  DoubleCollection dashes = new DoubleCollection();
  for (int i = 1; i < 10; i++)
    dashes.Add(0.1);
  drw.Pen.DashStyle = new DashStyle(dashes, 0);

  drw.Pen.EndLineCap = PenLineCap.Round;
  drw.Pen.StartLineCap = PenLineCap.Round;
  drw.Pen.DashCap = PenLineCap.Round;

  for (int i = 1; i < 10; i++)
  {
    LineGeometry myRectGeometry = new LineGeometry(new Point(1.1, i * 0.1), new Point(-0.1, i * 0.1));
    gg.Children.Add(myRectGeometry);
  }
}

Чтобы нарисовать линии пунктиром используется свойство Pen.DashStyle.
Свойства Pen.EndLineCap, Pen.StartLineCap, Pen.DashCap отвечают за то, как будут нарисованы концы линий. В данном случае выставлены закругленные концы.

Собственно построение двух кривых (одну – линией, другую – точками):

//график #1 - линия
if (DrawingStage == 3)
{

  drw.Brush = Brushes.White;
  drw.Pen = new Pen(Brushes.Black, 0.005);

  gg = new GeometryGroup();
  for (int i = 0; i < Np; i++)
  {
     LineGeometry l = new LineGeometry(new Point((double)i / (double)Np, 1.0 - (Data1[i] / 2.0)),
     new Point((double)(i + 1) / (double)Np, 1.0 - (Data1[i + 1] / 2.0)));
     gg.Children.Add(l);
  }
}

//график #2 - точки
if (DrawingStage == 4)
{

  drw.Brush = Brushes.White;
  drw.Pen = new Pen(Brushes.Black, 0.005);

  gg = new GeometryGroup();
  for (int i = 0; i < Np; i++)
  {
     EllipseGeometry el = new EllipseGeometry(new Point((double)i / (double)Np, 1.0 - (Data2[i] / 2.0)), 0.01, 0.01);
     gg.Children.Add(el);
  }
}

Наконец можно нарисовать сверху рамочку и сделать подписи:

//Обрезание лишнего
if (DrawingStage == 5)
{
  drw.Brush = Brushes.Transparent;
  drw.Pen = new Pen(Brushes.White, 0.2);

  RectangleGeometry myRectGeometry = new RectangleGeometry();
  myRectGeometry.Rect = new Rect(-0.1, -0.1, 1.2, 1.2);
  gg.Children.Add(myRectGeometry);

}

//Рамка
if (DrawingStage == 6)
{
  drw.Brush = Brushes.Transparent;
  drw.Pen = new Pen(Brushes.LightGray, 0.01);

  RectangleGeometry myRectGeometry = new RectangleGeometry();
  myRectGeometry.Rect = new Rect(0, 0, 1, 1);
  gg.Children.Add(myRectGeometry);
}

//Надписи
if (DrawingStage == 7)
{
  drw.Brush = Brushes.LightGray;
  drw.Pen = new Pen(Brushes.Gray, 0.003);

  for (int i = 1; i < 10; i++)
  {

     // Create a formatted text string.
    FormattedText formattedText = new FormattedText(
    ((double)(1-i*0.1)).ToString(),
    CultureInfo.GetCultureInfo("en-us"),
    FlowDirection.LeftToRight,
    new Typeface("Verdana"),
    0.05,
    Brushes.Black);

    // Set the font weight to Bold for the formatted text.
    formattedText.SetFontWeight(FontWeights.Bold);

    // Build a geometry out of the formatted text.
    Geometry geometry = formattedText.BuildGeometry(new Point(-0.1, i * 0.1 - 0.03));
    gg.Children.Add(geometry);
  }
}

Обратите внимание, что текст также переделывается в геометрию (formattedText.BuildGeometry).

Я сохранил проект с исходным кодом:
Рабочий пример для Visual Studio

  • ula

    Скажите, а как использовать для построения графика в WPF данные находящиеся в текстовом файле?

  • Массивы с данными Data1,Data2 можно заполнить примерно так:

    StreamReader sr = File.OpenText("data.txt");
    string input = "";
    int i =0;
    while ((input = sr.ReadLine()) != null)
    {
        Data1[i++] = double.Parse(input);
    }
    sr.Close();
  • ula

    Спасибо!
    Скажите, а если необходимо задавать двухмерный массив, т.е. значения Х и Y для построения графика?

  • А в чем именно проблема
    Просто сделать два массива X[] и Y[], считать в них из файла данные и в функции построения поменять, например:

    EllipseGeometry el = new EllipseGeometry(new Point(X[i], Y[i]), 0.01, 0.01);</p>
  • FFire

    А как написать текст по кривой, как бы по кругу?

  • Это честно говоря не делал. Нужно глянуть нет ли таких свойств у FormattedText, хотя скорее всего нет. Возможно это вообще нельзя сделать, кроме как через Transform (Translate, Rotate) каждую букву повернуть/подвинуть…

  • Dim

    Рисую график в WPF (около 4000 точек).
    При изменении размеров сильно тормозит окно. Как этот же график нарисовать, например, в BitmapImage и потом присвоить image.Source = myBitmap??? (ну, или как-то так 🙂

    Спасибо.

  • Dim
  • Скажите пожалуйста,а какую смысловую нагрузку несет цикл основной (который от 0 до 10 идет) ? почему нельзя сделать действия данные не в цикле, и зачем от 0 до 10,ведь судя по условиям, что внутри цикла берутся – достаточно чисел от 1 до 7(включительно если говорить).
    Я новичек в C# и в WPF , может просто чего-то не понимаю..)

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

  • Спасибо, Виктор!
    Я так и подумала, когда читала код! Поэтому данный вопрос и возник!
    Благодарю за ответ!

  • Ivan

    А если функции не синус и косинус, а какая либо произвольная, значения которой превосходят 1, как масштабировать прямоугольник в данном случаи квадрат 1×1, что бы линия была на фоне прямоугольника, а не за ее пределами?

    за ранее спасибо!

    • Найти максимум и минимум фунции на интервале, округлить, А потом стоить в этом диапазоне.

  • Иван

    Спасибо, а ещё вопрос, следующий, а если я хочу что бы при растягивании окна, график пропорционально растягивался, т.е. не как квадрат, а как прямоугольник! А то этот растягивается всегда как квадрат?

    • Отрисованная геометрия в случае графика будет искажаться при ресайзе – то есть просто нужно перерисовывать все при событии масштабирования на основе текущих размеров окна.