WPF在Canvas中绘图实现折线统计图
最近在WPF中做一个需要实现统计的功能,其中需要用到统计图,之前也没有接触过,度娘上大多都是各种收费或者免费的第三方控件,不想用第三方控件那就自己画一个吧。
在园子还找到一篇文章,思路来自这篇文章,文章链接:http://www.cnblogs.com/endlesscoding/p/6670432.html
不过根据我的需求,数据每次都在变化,所以都只能从后台绑定,先来看一下完成后的效果吧
可以看到,数据源是一年内一到十二月的金额,所以X轴是固定的,而Y轴标尺是根据数据源的最大值向上取100。再来计算每个标尺该显示的数值。
数据点的显示,也是根据提供数据的比例,来计算出像素值的
从头开始吧,先来说xaml,xaml中需要一个Canvas控件,之后所有的图形就是画在这里面
不会用Canvas的话可以先学习下官方文档:https://msdn.microsoft.com/zh-cn/library/system.windows.controls.canvas(v=vs.110).aspx
1 <Grid Height="400" Width="645"> 2 <Grid.ColumnDefinitions> 3 <ColumnDefinition Width="150" /> 4 <ColumnDefinition Width="330"/> 5 <ColumnDefinition Width="*"/> 6 </Grid.ColumnDefinitions> 7 <Grid.RowDefinitions> 8 <RowDefinition Height="25" /> 9 <RowDefinition /> 10 </Grid.RowDefinitions> 11 <j:JLabel Label="企业账号:" Grid.Column="0" Grid.Row="0"> 12 <TextBlock Text="{Binding Userid}" HorizontalAlignment="Left" Foreground="Red"/> 13 </j:JLabel> 14 <j:JLabel Label="企业名称:" Grid.Column="1" Grid.Row="0"> 15 <TextBlock Text="{Binding Username}" HorizontalAlignment="Left" Foreground="Red"/> 16 </j:JLabel> 17 <j:JLabel Label="总金额(元):" Grid.Column="2" Grid.Row="0"> 18 <TextBlock Text="{Binding Pay_Total}" HorizontalAlignment="Left" Foreground="Red"/> 19 </j:JLabel> 20 <Canvas x:Name="chartCanvas" Margin="5" Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="4"> 21 </Canvas> 22 </Grid>
先来画横纵坐标和箭头吧,x1,y1,x2,y2这四个参数是Line在Canvas中的起点终点位置像素值
同样,Line类官方文档:https://msdn.microsoft.com/zh-cn/library/system.windows.shapes.line(v=vs.110).aspx
1 /// <summary> 2 /// 生成横纵坐标及箭头 3 /// </summary> 4 private void DrawArrow() 5 { 6 Line x_axis = new Line();//x轴 7 Line y_axis = new Line();//y轴 8 x_axis.Stroke = System.Windows.Media.Brushes.Black; 9 y_axis.Stroke = System.Windows.Media.Brushes.Black; 10 x_axis.StrokeThickness = 3; 11 y_axis.StrokeThickness = 3; 12 x_axis.X1 = 40; 13 x_axis.Y1 = 320; 14 x_axis.X2 = 600; 15 x_axis.Y2 = 320; 16 y_axis.X1 = 40; 17 y_axis.Y1 = 320; 18 y_axis.X2 = 40; 19 y_axis.Y2 = 30; 20 this.chartCanvas.Children.Add(x_axis); 21 this.chartCanvas.Children.Add(y_axis); 22 23 Line y_scale1 = new Line(); //坐标原点直角 24 y_scale1.Stroke = System.Windows.Media.Brushes.Black; 25 y_scale1.StrokeThickness =1; 26 y_scale1.X1 = 40; 27 y_scale1.Y1 = 310; 28 y_scale1.X2 = 44; 29 y_scale1.Y2 = 310; 30 y_scale1.StrokeStartLineCap = PenLineCap.Triangle; 31 this.chartCanvas.Children.Add(y_scale1); 32 33 Path x_axisArrow = new Path();//x轴箭头 34 Path y_axisArrow = new Path();//y轴箭头 35 x_axisArrow.Fill = new SolidColorBrush(Color.FromRgb(0, 0, 0)); 36 y_axisArrow.Fill = new SolidColorBrush(Color.FromRgb(0, 0, 0)); 37 PathFigure x_axisFigure = new PathFigure(); 38 x_axisFigure.IsClosed = true; 39 x_axisFigure.StartPoint = new Point(600, 316); //路径的起点 40 x_axisFigure.Segments.Add(new LineSegment(new Point(600, 324), false)); //第2个点 41 x_axisFigure.Segments.Add(new LineSegment(new Point(610, 320), false)); //第3个点 42 PathFigure y_axisFigure = new PathFigure(); 43 y_axisFigure.IsClosed = true; 44 y_axisFigure.StartPoint = new Point(36, 30); //路径的起点 45 y_axisFigure.Segments.Add(new LineSegment(new Point(44, 30), false)); //第2个点 46 y_axisFigure.Segments.Add(new LineSegment(new Point(40, 20), false)); //第3个点 47 PathGeometry x_axisGeometry = new PathGeometry(); 48 PathGeometry y_axisGeometry = new PathGeometry(); 49 x_axisGeometry.Figures.Add(x_axisFigure); 50 y_axisGeometry.Figures.Add(y_axisFigure); 51 x_axisArrow.Data = x_axisGeometry; 52 y_axisArrow.Data = y_axisGeometry; 53 this.chartCanvas.Children.Add(x_axisArrow); 54 this.chartCanvas.Children.Add(y_axisArrow); 55 56 TextBlock x_label =new TextBlock(); 57 TextBlock y_label =new TextBlock(); 58 TextBlock o_label =new TextBlock(); 59 x_label.Text = "月"; 60 y_label.Text = "元"; 61 o_label.Text = "0"; 62 Canvas.SetLeft(x_label, 610); 63 Canvas.SetLeft(y_label, 20); 64 Canvas.SetLeft(o_label, 20); 65 Canvas.SetTop(x_label, 317); 66 Canvas.SetTop(y_label, 4); 67 Canvas.SetTop(o_label, 312); 68 x_label.FontSize = 14; 69 y_label.FontSize = 14; 70 o_label.FontSize = 14; 71 this.chartCanvas.Children.Add(x_label); 72 this.chartCanvas.Children.Add(y_label); 73 this.chartCanvas.Children.Add(o_label); 74 75 }
标尺,X轴以45为间隔单位,Y轴以10px为单位,且没5格显示一个大标尺
1 /// <summary> 2 /// 作出x轴和y轴的标尺 3 /// </summary> 4 private void DrawScale() 5 { 6 for (int i = 1; i < 13; i++)//作12个刻度 7 { 8 //原点 O=(40,320) 9 Line x_scale = new Line(); //主x轴标尺 10 x_scale.StrokeEndLineCap = PenLineCap.Triangle; 11 x_scale.StrokeThickness = 1; 12 x_scale.Stroke = new SolidColorBrush(Color.FromRgb(0, 0, 0)); 13 x_scale.X1 = 40 + i * 45; 14 x_scale.X2 = x_scale.X1; 15 x_scale.Y1 = 320; 16 x_scale.StrokeThickness = 3; 17 x_scale.Y2 = x_scale.Y1 - 8; 18 this.chartCanvas.Children.Add(x_scale); 19 20 Line x_in = new Line();//x轴轴辅助标尺 21 x_in.Stroke = System.Windows.Media.Brushes.LightGray; 22 x_in.StrokeThickness = 0.5; 23 x_in.X1 = 40 + i * 45; 24 x_in.Y1 = 320; 25 x_in.X2 = 40 + i * 45; 26 x_in.Y2 = 30; 27 this.chartCanvas.Children.Add(x_in); 28 } 29 for (int j = 0; j < 30; j++ ) 30 { 31 Line y_scale = new Line(); //主Y轴标尺 32 y_scale.StrokeEndLineCap = PenLineCap.Triangle; 33 y_scale.StrokeThickness = 1; 34 y_scale.Stroke = new SolidColorBrush(Color.FromRgb(0, 0, 0)); 35 36 y_scale.X1 = 40; //原点x=40 37 if (j % 5 == 0) 38 { 39 y_scale.StrokeThickness = 3; 40 y_scale.X2 = y_scale.X1 + 8;//大刻度线 41 } 42 else 43 { 44 y_scale.X2 = y_scale.X1 + 4;//小刻度线 45 } 46 47 y_scale.Y1 = 320 - j * 10; //每10px作一个刻度 48 y_scale.Y2 = y_scale.Y1; 49 this.chartCanvas.Children.Add(y_scale); 50 } 51 for (int i = 1; i < 6; i++) 52 { 53 Line y_in = new Line();//y轴辅助标尺 54 y_in.Stroke = System.Windows.Media.Brushes.LightGray; 55 y_in.StrokeThickness = 0.5; 56 y_in.X1 = 40; 57 y_in.Y1 = 320 - i * 50; 58 y_in.X2 = 600; 59 y_in.Y2 = 320 - i * 50; 60 this.chartCanvas.Children.Add(y_in); 61 } 62 63 }
刻度标签的话,X轴是固定的,并且其中用到了一个把阿拉伯数字转换为中文的方法 NumberToChinese(),
Y轴标尺标签,是用出入的 list<double>,计算出最大值再向上取100整,再分成五份,每份的值就是五个标签了
list最大值向上取100的方法:(除100向上取整再乘100)
Math.Ceiling(list.Max() / 100) * 100
1 /// <summary> 2 /// 添加刻度标签 3 /// </summary> 4 private void DrawScaleLabel(List<double> list) 5 { 6 for (int i = 1; i < 13; i++) 7 { 8 TextBlock x_ScaleLabel = new TextBlock(); 9 x_ScaleLabel.Text = NumberToChinese(i.ToString()); 10 if (x_ScaleLabel.Text == "一零") 11 { 12 x_ScaleLabel.Text = "十"; 13 Canvas.SetLeft(x_ScaleLabel, 40 + 45 * i - 6); 14 } 15 else if (x_ScaleLabel.Text == "一一") 16 { 17 x_ScaleLabel.Text = "十一"; 18 Canvas.SetLeft(x_ScaleLabel, 40 + 45 * i - 10); 19 } 20 21 else if (x_ScaleLabel.Text == "一二") 22 { 23 x_ScaleLabel.Text = "十二"; 24 Canvas.SetLeft(x_ScaleLabel, 40 + 45 * i - 10); 25 } 26 else 27 { 28 Canvas.SetLeft(x_ScaleLabel, 40 + 45 * i - 6); 29 } 30 Canvas.SetTop(x_ScaleLabel, 320 + 2); 31 this.chartCanvas.Children.Add(x_ScaleLabel); 32 } 33 34 for (int i = 1; i < 6; i++) 35 { 36 TextBlock y_ScaleLabel = new TextBlock(); 37 double max = Math.Ceiling(list.Max() / 100) * 100; 38 y_ScaleLabel.Text = (i * (max/5)).ToString(); 39 Canvas.SetLeft(y_ScaleLabel, 40 - 30); 40 Canvas.SetTop(y_ScaleLabel, 320 - 5 * 10 * i - 6); 41 42 this.chartCanvas.Children.Add(y_ScaleLabel); 43 } 44 } 45 46 /// <summary> 47 /// 数字转汉字 48 /// </summary> 49 /// <param name="numberStr"></param> 50 /// <returns></returns> 51 public static string NumberToChinese(string numberStr) 52 { 53 string numStr = "0123456789"; 54 string chineseStr = "零一二三四五六七八九"; 55 char[] c = numberStr.ToCharArray(); 56 for (int i = 0; i < c.Length; i++) 57 { 58 int index = numStr.IndexOf(c[i]); 59 if (index != -1) 60 c[i] = chineseStr.ToCharArray()[index]; 61 } 62 numStr = null; 63 chineseStr = null; 64 return new string(c); 65 }
接下来就是计算数据点了,难点在于计算像素点,X轴是固定的,所以不用关注
直接算好的X轴十二个数值
double[] left = { 85, 130, 175, 220, 265, 310, 355, 400, 445, 490, 535, 580 };
而Y轴就要自己算了,提供一个思路:
区域总像素 - 区域总像素 * (数值/最大值)
1 /// <summary> 2 /// 计算数据点并添加 3 /// </summary> 4 /// <param name="list"></param> 5 private void DrawPoint(List<double> list) 6 { 7 double[] left = { 85, 130, 175, 220, 265, 310, 355, 400, 445, 490, 535, 580 }; 8 List<double> leftlist = new List<double>(); 9 leftlist.AddRange(left); 10 11 for (int i = 1; i < 13; i++) 12 { 13 Ellipse Ellipse = new Ellipse(); 14 Ellipse .Fill = new SolidColorBrush(Color.FromRgb(0, 0, 0xff)); 15 Ellipse .Width = 8; 16 Ellipse .Height = 8; 17 Canvas.SetLeft(Ellipse,leftlist[i-1]- 4); 18 double y_Max = Math.Ceiling(list.Max() / 100) * 100; 19 Canvas.SetTop(Ellipse, 320 - 250 * (list[i-1] / y_Max) - 4); 20 coordinatePoints.Add(new Point(leftlist[i-1], 320 - 250 * (list[i-1] / y_Max))); 21 this.chartCanvas.Children.Add(Ellipse); 22 //值显示 23 TextBlock EP_Label = new TextBlock(); 24 EP_Label.Foreground = System.Windows.Media.Brushes.Red; 25 EP_Label.Text = list[i-1].ToString(); 26 Canvas.SetLeft(EP_Label, leftlist[i-1] - 10); 27 Canvas.SetTop(EP_Label, 320 - 250 * (list[i-1] / y_Max) - 20); 28 this.chartCanvas.Children.Add(EP_Label); 29 } 30 }
在绘制数据点的时候,每一次的位置都保存了: coordinatePoints.Add(new Point(leftlist[i-1], 320 - 250 * (list[i-1] / y_Max)));
先得定义:
/// <summary> /// 折线图坐标点 /// </summary> private PointCollection coordinatePoints = new PointCollection();
最后直接连连看就好了
1 /// <summary> 2 /// 绘制连接折线 3 /// </summary> 4 private void DrawCurve() 5 { 6 Polyline curvePolyline = new Polyline(); 7 8 curvePolyline.Stroke = Brushes.Green; 9 curvePolyline.StrokeThickness = 2; 10 11 curvePolyline.Points = coordinatePoints; 12 this.chartCanvas.Children.Add(curvePolyline); 13 }
由于我项目的关系,数据是从DataGrid控件行数据来的,所以每一次都不一样,只能在弹出窗体时调用这几个方法
由于每一都不一样,在窗口关闭时需要清空画布内的所有控件,否则画布内控件会一直覆盖
chartCanvas.Children.Clear();
coordinatePoints.Clear();
我的邮箱:alonezying@163.com 欢迎交流学习