上一小节讲解了动态生成折线图和区域图,对于简单的图形这样通过C#代码来生成的方式是很方便的,但是当我们的图表要实现更加复杂的逻辑的时候,这种动态生成的方式就显得力不从心了,那就需要利用控件封装的方式来实现更加强大的图表控件功能。这一小节将来讲解怎样去用封装控件的方式去实现图表,用一个饼图控件作为例子进行分析讲解。
饼图其实就是把一个圆形分成若干块,每一块代表着一个类别的数据,可以把这每一块的图形看作是饼图片形形状。要实现一个饼图控件,首先需要做的就是要实现饼图片形形状,在第4章里面讲解了实现如何实现自定义的形状,饼图片形形状也可以通过这种方式来实现。饼图片形形状有一些重要的属性,如饼图半径Radius,内圆半径InnerRadius,旋转角度RotationAngle,片形角度WedgeAngle,点innerArcStartPoint,点innerArcEndPoint,点outerArcStartPoint和点outerArcEndPoint等,这些属性的含义如图13.5所示。要绘制出这个饼图片形形状需要计算出4个点的坐标(点innerArcStartPoint,点innerArcEndPoint,点outerArcStartPoint和点outerArcEndPoint),这4的点的坐标需要通过半径和角度相关的属性计算出来。计算出这4个点的坐标的坐标之后,然后通过这4个点创建一个Path图形,这个Path图形由两条直线和两条弧线组成,形成了一个饼图片形形状。通过这种方式不仅仅把这个饼图片形形状创建好了,连这个图形在整个饼图的位置也设置好了。代码如下所示。
代码清单5-2 : 饼图图表(源代码:第5章/Examples_5_2)
PiePiece.cs文件代码:自定义的饼图片形形状 ------------------------------------------------------------------------------------------------------------------ using System; using Windows.Foundation; using Windows.UI.Xaml; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Shapes; namespace PieChartDemo { /// <summary> /// 自定义的饼图片形形状 /// </summary> class PiePiece : Path { #region 依赖属性 // 注册半径属性 public static readonly DependencyProperty RadiusProperty = DependencyProperty.Register("RadiusProperty", typeof(double), typeof(PiePiece), new PropertyMetadata(0.0)); // 饼图半径 public double Radius { get { return (double)GetValue(RadiusProperty); } set { SetValue(RadiusProperty, value); } } // 注册饼图片形点击后推出的距离 public static readonly DependencyProperty PushOutProperty = DependencyProperty.Register("PushOutProperty", typeof(double), typeof(PiePiece), new PropertyMetadata(0.0)); // 距离饼图中心的距离 public double PushOut { get { return (double)GetValue(PushOutProperty); } set { SetValue(PushOutProperty, value); } } // 注册饼图内圆半径属性 public static readonly DependencyProperty InnerRadiusProperty = DependencyProperty.Register("InnerRadiusProperty", typeof(double), typeof(PiePiece), new PropertyMetadata(0.0)); // 饼图内圆半径 public double InnerRadius { get { return (double)GetValue(InnerRadiusProperty); } set { SetValue(InnerRadiusProperty, value); } } // 注册饼图片形的角度属性 public static readonly DependencyProperty WedgeAngleProperty = DependencyProperty.Register("WedgeAngleProperty", typeof(double), typeof(PiePiece), new PropertyMetadata(0.0)); // 饼图片形的角度 public double WedgeAngle { get { return (double)GetValue(WedgeAngleProperty); } set { SetValue(WedgeAngleProperty, value); this.Percentage = (value / 360.0); } } // 注册饼图片形旋转角度的属性 public static readonly DependencyProperty RotationAngleProperty = DependencyProperty.Register("RotationAngleProperty", typeof(double), typeof(PiePiece), new PropertyMetadata(0.0)); // 旋转的角度 public double RotationAngle { get { return (double)GetValue(RotationAngleProperty); } set { SetValue(RotationAngleProperty, value); } } // 注册中心点的X坐标属性 public static readonly DependencyProperty CentreXProperty = DependencyProperty.Register("CentreXProperty", typeof(double), typeof(PiePiece), new PropertyMetadata(0.0)); // 中心点的X坐标 public double CentreX { get { return (double)GetValue(CentreXProperty); } set { SetValue(CentreXProperty, value); } } // 注册中心点的Y坐标属性 public static readonly DependencyProperty CentreYProperty = DependencyProperty.Register("CentreYProperty", typeof(double), typeof(PiePiece), new PropertyMetadata(0.0)); // 中心点的Y坐标 public double CentreY { get { return (double)GetValue(CentreYProperty); } set { SetValue(CentreYProperty, value); } } // 注册该饼图片形所占饼图的百分比属性 public static readonly DependencyProperty PercentageProperty = DependencyProperty.Register("PercentageProperty", typeof(double), typeof(PiePiece), new PropertyMetadata(0.0)); // 饼图片形所占饼图的百分比 public double Percentage { get { return (double)GetValue(PercentageProperty); } private set { SetValue(PercentageProperty, value); } } // 注册该饼图片形所代表的数值属性 public static readonly DependencyProperty PieceValueProperty = DependencyProperty.Register("PieceValueProperty", typeof(double), typeof(PiePiece), new PropertyMetadata(0.0)); // 该饼图片形所代表的数值 public double PieceValue { get { return (double)GetValue(PieceValueProperty); } set { SetValue(PieceValueProperty, value); } } #endregion public PiePiece() { CreatePathData(0, 0); } private double lastWidth = 0; private double lastHeight = 0; private PathFigure figure; // 在图形中添加一个点 private void AddPoint(double x, double y) { LineSegment segment = new LineSegment(); segment.Point = new Point(x + 0.5 * StrokeThickness, y + 0.5 * StrokeThickness); figure.Segments.Add(segment); } // 在图形中添加一条线段 private void AddLine(Point point) { LineSegment segment = new LineSegment(); segment.Point = point; figure.Segments.Add(segment); } // 在图形中添加一个圆弧 private void AddArc(Point point, Size size, bool largeArc, SweepDirection sweepDirection) { ArcSegment segment = new ArcSegment(); segment.Point = point; segment.Size = size; segment.IsLargeArc = largeArc; segment.SweepDirection = sweepDirection; figure.Segments.Add(segment); } private void CreatePathData(double width, double height) { // 用于退出布局的循环逻辑 if (lastWidth == width && lastHeight == height) return; lastWidth = width; lastHeight = height; Point startPoint = new Point(CentreX, CentreY); // 计算饼图片形内圆弧的开始点 Point innerArcStartPoint = ComputeCartesianCoordinate(RotationAngle, InnerRadius); // 根据中心点来校正坐标的位置 innerArcStartPoint = Offset(innerArcStartPoint,CentreX, CentreY); // 计算饼图片形内圆弧的结束点 Point innerArcEndPoint = ComputeCartesianCoordinate(RotationAngle + WedgeAngle, InnerRadius); innerArcEndPoint = Offset(innerArcEndPoint, CentreX, CentreY); // 计算饼图片形外圆弧的开始点 Point outerArcStartPoint = ComputeCartesianCoordinate(RotationAngle, Radius); outerArcStartPoint = Offset(outerArcStartPoint, CentreX, CentreY); // 计算饼图片形外圆弧的结束点 Point outerArcEndPoint = ComputeCartesianCoordinate(RotationAngle + WedgeAngle, Radius); outerArcEndPoint = Offset(outerArcEndPoint, CentreX, CentreY); // 判断饼图片形的角度是否大于180度 bool largeArc = WedgeAngle > 180.0; // 把扇面饼图往偏离中心点推出一部分 if (PushOut > 0) { Point offset = ComputeCartesianCoordinate(RotationAngle + WedgeAngle / 2, PushOut); // 根据偏移量来重新设置圆弧的坐标 innerArcStartPoint = Offset(innerArcStartPoint,offset.X, offset.Y); innerArcEndPoint = Offset(innerArcEndPoint,offset.X, offset.Y); outerArcStartPoint = Offset(outerArcStartPoint,offset.X, offset.Y); outerArcEndPoint = Offset(outerArcEndPoint,offset.X, offset.Y); } // 外圆的大小 Size outerArcSize = new Size(Radius, Radius); // 内圆的大小 Size innerArcSize = new Size(InnerRadius, InnerRadius); var geometry = new PathGeometry(); figure = new PathFigure(); // 从内圆开始坐标开始画一个闭合的扇形图形 figure.StartPoint = innerArcStartPoint; AddLine(outerArcStartPoint); AddArc(outerArcEndPoint, outerArcSize, largeArc, SweepDirection.Clockwise); AddLine(innerArcEndPoint); AddArc(innerArcStartPoint, innerArcSize, largeArc, SweepDirection.Counterclockwise); figure.IsClosed = true; geometry.Figures.Add(figure); this.Data = geometry; } protected override Size MeasureOverride(Size availableSize) { return availableSize; } protected override Size ArrangeOverride(Size finalSize) { CreatePathData(finalSize.Width, finalSize.Height); return finalSize; } //把点进行偏移转换 private Point Offset(Point point, double offsetX, double offsetY) { point.X += offsetX; point.Y += offsetY; return point; } /// <summary> /// 根据角度和半径来计算出圆弧上的点的坐标 /// </summary> /// <param name="angle">角度</param> /// <param name="radius">半径</param> /// <returns>圆弧上的点坐标</returns> private Point ComputeCartesianCoordinate(double angle, double radius) { // 转换成弧度单位 double angleRad = (Math.PI / 180.0) * (angle - 90); double x = radius * Math.Cos(angleRad); double y = radius * Math.Sin(angleRad); return new Point(x, y); } } }
创建好了PiePiece形状之后,下面就要开始创建利用PiePiece形状来创建饼图控件了。创建饼图控件是通过UserControl控件来实现,UserControl控件的XAML代码里面只有一个Grid面板,是用来加载PiePiece形状来组成饼图。XAML代码如下所示:
PiePlotter.xaml文件代码 ------------------------------------------------------------------------------------------------------------------ <UserControl x:Class="PieChartDemo.PiePlotter" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeNormal}" Foreground="{StaticResource PhoneForegroundBrush}" d:DesignHeight="480" d:DesignWidth="480" > <Grid x:Name="LayoutRoot"></Grid> </UserControl>
在实现饼图之前,需要知道饼图里面的数据集合的,还需要用一个实体类PieDataItem来表示饼图的数据项,有两个属性一个是表示图形的数值Value属性,另外一个是表示饼图片形块的颜色Brush属性。PieDataItem代码如下:
PieDataItem.cs文件代码 ------------------------------------------------------------------------------------------------------------------ using Windows.UI.Xaml.Media; namespace PieChartDemo { /// <summary> /// 饼图数据实体 /// </summary> public class PieDataItem { public double Value { get; set; } public SolidColorBrush Brush { get; set; } } }
下面来实现饼图控件加载的逻辑,在饼图控件里面还需要自定义一些相关的属性,用来传递相关的参数。属性HoleSize表示饼图内圆的大小,按照比例来计算;属性PieWidth表示饼图的宽度。饼图的数据集合是通过控件的数据上下文属性DataContext属性来传递,在初始化饼图的时候需要把DataContext的数据读取出来然后再创建PiePiece图形。每个PiePiece图形都添加了Tap事件,用来实现当用户点击饼图的时候,相应的某一块回往外推出去。代码如下所示:
PiePlotter.xaml.cs文件代码 ------------------------------------------------------------------------------------------------------------------ using System.Collections.Generic; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Input; namespace PieChartDemo { /// <summary> /// 饼图控件 /// </summary> public partial class PiePlotter : UserControl { #region dependency properties // 注册内圆大小属性 public static readonly DependencyProperty HoleSizeProperty = DependencyProperty.Register("HoleSize", typeof(double), typeof(PiePlotter), new PropertyMetadata(0.0)); // 内圆的大小,按照比例来计算 public double HoleSize { get { return (double)GetValue(HoleSizeProperty); } set { SetValue(HoleSizeProperty, value); } } // 注册饼图宽度属性 public static readonly DependencyProperty PieWidthProperty = DependencyProperty.Register("PieWidth", typeof(double), typeof(PiePlotter), new PropertyMetadata(0.0)); // 饼图宽度 public double PieWidth { get { return (double)GetValue(PieWidthProperty); } set { SetValue(PieWidthProperty, value); } } #endregion // 饼图的片形PiePiece的集合 private List<PiePiece> piePieces = new List<PiePiece>(); // 选中的当前饼图的数据项 private PieDataItem CurrentItem; public PiePlotter() { InitializeComponent(); } // 初始化展示饼图的方法 public void ShowPie() { // 获取控件的数据上下文,转化成数据集合 List<PieDataItem> myCollectionView = (List<PieDataItem>)this.DataContext; if (myCollectionView == null) return; // 半径的大小 double halfWidth = PieWidth / 2; // 内圆半径大小 double innerRadius = halfWidth * HoleSize; // 计算图表数据的总和 double total = 0; foreach (PieDataItem item in myCollectionView) { total += item.Value; } // 通过PiePiece构建饼图 LayoutRoot.Children.Clear(); piePieces.Clear(); double accumulativeAngle = 0; foreach (PieDataItem item in myCollectionView) { bool selectedItem = item == CurrentItem; double wedgeAngle = item.Value * 360 / total; // 根据数据来创建饼图的每一块图形 PiePiece piece = new PiePiece() { Radius = halfWidth, InnerRadius = innerRadius, CentreX = halfWidth, CentreY = halfWidth, PushOut = (selectedItem ? 10.0 : 0), WedgeAngle = wedgeAngle, PieceValue = item.Value, RotationAngle = accumulativeAngle, Fill = item.Brush, Tag = item }; // 添加饼图片形的点击事件 piece.Tapped += piece_Tapped; piePieces.Add(piece); LayoutRoot.Children.Add(piece); accumulativeAngle += wedgeAngle; } } void piece_Tapped(object sender, TappedRoutedEventArgs e) { PiePiece piePiece = sender as PiePiece; CurrentItem = piePiece.Tag as PieDataItem; ShowPie(); } } }
在调用饼图控件时需要引用控件所属的空间,然后在XAML上调用饼图控件。
MainPage.xaml文件主要代码 ------------------------------------------------------------------------------------------------------------------ ……省略若干代码 xmlns:local="using:PieChartDemo" ……省略若干代码 <local:PiePlotter x:Name="piePlotter" Width="400" Height="400" PieWidth="400" HoleSize="0.2"></local:PiePlotter>
在C#代码里面,对饼图的DataContext属性进行赋值饼图的数据集合,然后调用ShowPie方法初始化饼图。代码如下:
MainPage.xaml.cs文件主要代码 ------------------------------------------------------------------------------------------------------------------ public MainPage() { InitializeComponent(); List<PieDataItem> datas=new List<PieDataItem>(); datas.Add(new PieDataItem{ Value=30, Brush = new SolidColorBrush(Colors.Red)}); datas.Add(new PieDataItem { Value = 40, Brush = new SolidColorBrush(Colors.Orange) }); datas.Add(new PieDataItem { Value = 50, Brush = new SolidColorBrush(Colors.Blue) }); datas.Add(new PieDataItem { Value = 30, Brush = new SolidColorBrush(Colors.LightGray) }); datas.Add(new PieDataItem { Value = 20, Brush = new SolidColorBrush(Colors.Purple) }); datas.Add(new PieDataItem { Value = 40, Brush = new SolidColorBrush(Colors.Green) }); piePlotter.DataContext = datas; piePlotter.ShowPie(); }