Построение графиков на 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