簡介
前段時間幫一個同事的忙,利用WPF實現的一個簡單餅圖,僅能看餅圖的比例,無文字查看功能。效果圖如下:
用法:
var sectorParts = new List<SectorPart>(); sectorParts.Add(new SectorPart(90, Brushes.Red)); sectorParts.Add(new SectorPart(30, Brushes.Green)); sectorParts.Add(new SectorPart(120, Brushes.GreenYellow)); sectorParts.Add(new SectorPart(70, Brushes.HotPink)); sectorParts.Add(new SectorPart(50, Brushes.Yellow)); var ringParts = new List<RingPart>(); ringParts.Add(new RingPart(40, 20, 40, 20, Brushes.White)); var shapes = PieChartDrawer.GetEllipsePieChartShapes(midPoint, 180, 90, 30, sectorParts, ringParts); foreach (var shape in shapes) { GrdPie.Children.Add(shape); }
設置好餅圖相關的信息,獲取其各個組成部分,再將其添加到容器中。
原理
可以看出簡介中的圖由一系列中扇形和環組成,計算出扇形和環的形狀就可以完成餅圖的繪制了。
扇形
一個扇形由兩邊和一條弧組成,扇形的關鍵就在已知圓周的圓形和半徑,以及扇形的邊繞Y軸正向旋轉的角度,如何求出扇形在圓周上的點。
在才開始我走了不少彎路,利用矩陣做了許多運算,結果都不對。后面靈機一動,發現不用那么麻煩。直接把圓平移至原點,計算出相應扇形在圓周上的點,再將其平移回來即可。代碼如下:
/// <summary> /// 獲取圓周上指定角度的點坐標 /// </summary> /// <param name="center">圓心</param> /// <param name="radius">半徑</param> /// <param name="angle">角度,從0到360度,以正北方向為0度,順時針旋轉角度增加</param> /// <returns>在圓周上旋轉角度后的坐標</returns> public static Point GetCirclePoint(this Point center, double radius, double angle) { // 圓心平移到原點后0度所對應的向量 var zeroAngleVector = new Vector(0, radius); // 旋轉角度所對應的矩陣 var rotateMatrix = new Matrix(); rotateMatrix.Rotate(180 + angle); // 因旋轉的中心點在原點,最后需要平移到實際坐標上 return (zeroAngleVector * rotateMatrix) + center; }
有了圓的計算方法,橢圓的也就水到渠成了,因為橢圓相當於圓的拉伸或者收縮。
/// <summary> /// 獲取橢圓上指定角度的點坐標 /// </summary> /// <param name="center">橢圓兩焦點的中點</param> /// <param name="radiusX">長軸</param> /// <param name="radiusY">短軸</param> /// <param name="angle">角度,從0到360度,以正北方向為0度,順時針旋轉角度增加</param> /// <returns>在橢圓上旋轉角度后的坐標</returns> public static Point GetEllipsePoint(this Point center, double radiusX, double radiusY, double angle) { // 半徑為X半軸的圓圓心平移到原點后0度所對應的向量 var circleZeroAnglePoint = new Vector(0, radiusX); // 旋轉角度所對應的矩陣 var rotateMatrix = new Matrix(); rotateMatrix.Rotate(180 + angle); // 圓旋轉角度后的坐標 var circlePoint = circleZeroAnglePoint * rotateMatrix; // 將圓拉伸橢圓后的坐標 var ellpseOrigin = new Point(circlePoint.X, circlePoint.Y * radiusY / radiusX); // 將坐標平移至實際坐標 return (Vector)ellpseOrigin + center; }
環
環可由兩個橢圓計算出來。較簡單,就不多說了。
代碼
扇形的定義
namespace PieChartTest { using System.Windows.Media; /// <summary> /// 扇形 /// </summary> public class SectorPart { /// <summary> /// 構造函數 /// </summary> /// <param name="spanAngle">跨越角度</param> /// <param name="fillBrush">填充畫刷</param> public SectorPart(double spanAngle, Brush fillBrush) { this.SpanAngle = spanAngle; this.FillBrush = fillBrush; } /// <summary> /// 跨越角度,單位為角度,取值范圍為0到360 /// </summary> public double SpanAngle { get; set; } /// <summary> /// 填充畫刷 /// </summary> public Brush FillBrush { get; set; } } }
環的定義
namespace PieChartTest { using System.Windows.Media; /// <summary> /// 環 /// </summary> public class RingPart { /// <summary> /// 構造函數,構造里外均為圓的圓環 /// </summary> /// <param name="radius">里圓半徑</param> /// <param name="spanRadius">里外圓半徑差</param> /// <param name="fillBrush">填充畫刷</param> public RingPart(double radius, double spanRadius, Brush fillBrush) { this.RadiusX = radius; this.RadiusY = radius; this.SpanRadiusX = spanRadius; this.SpanRadiusY = spanRadius; this.FillBrush = fillBrush; } /// <summary> /// 構造函數 /// </summary> /// <param name="radiusX">里邊橢圓的長軸</param> /// <param name="radiusY">里邊橢圓的短軸</param> /// <param name="spanRadiusX">里外橢圓的長軸差</param> /// <param name="spanRadiusY">里外橢圓的短軸差</param> /// <param name="fillBrush">填充畫刷</param> public RingPart(double radiusX, double radiusY, double spanRadiusX, double spanRadiusY, Brush fillBrush) { this.RadiusX = radiusX; this.RadiusY = radiusY; this.SpanRadiusX = spanRadiusX; this.SpanRadiusY = spanRadiusY; this.FillBrush = fillBrush; } /// <summary> /// 長軸 /// </summary> public double RadiusX { get; set; } /// <summary> /// 短軸 /// </summary> public double RadiusY { get; set; } /// <summary> /// 長軸跨越的距離 /// </summary> public double SpanRadiusX { get; set; } /// <summary> /// 短軸跨越的距離 /// </summary> public double SpanRadiusY { get; set; } /// <summary> /// 填充畫刷 /// </summary> public Brush FillBrush { get; set; } } }
餅圖的繪制類
namespace PieChartTest { using System.Collections.Generic; using System.Windows; using System.Windows.Media; using System.Windows.Shapes; /// <summary> /// 餅圖的繪制類 /// </summary> public static class PieChartDrawer { /// <summary> /// 獲取餅圖的形狀列表 /// </summary> /// <param name="center">圓心</param> /// <param name="radius">圓的半徑</param> /// <param name="offsetAngle">偏移角度,即第一個扇形開始的角度</param> /// <param name="sectorParts">扇形列表,扇形列表的SpanAngle之和應為360度</param> /// <param name="ringParts">環列表</param> /// <returns>構成餅圖的形狀列表</returns> public static IEnumerable<Shape> GetPieChartShapes(Point center, double radius, double offsetAngle, IEnumerable<SectorPart> sectorParts, IEnumerable<RingPart> ringParts) { return GetEllipsePieChartShapes(center, radius, radius, offsetAngle, sectorParts, ringParts); } /// <summary> /// 獲取橢圓形狀的餅圖的形狀列表 /// </summary> /// <param name="center">橢圓兩個焦點的中點</param> /// <param name="radiusX">橢圓的長軸</param> /// <param name="radiusY">橢圓的短軸</param> /// <param name="offsetAngle">偏移角度,即第一個扇形開始的角度</param> /// <param name="sectorParts">扇形列表,扇形列表的SpanAngle之和應為360度</param> /// <param name="ringParts">環列表</param> /// <returns>構成餅圖的形狀列表</returns> public static IEnumerable<Shape> GetEllipsePieChartShapes(Point center, double radiusX, double radiusY, double offsetAngle, IEnumerable<SectorPart> sectorParts, IEnumerable<RingPart> ringParts) { var shapes = new List<Shape>(); double startAngle = offsetAngle; foreach (var sectorPart in sectorParts) { // 扇形順時針方向在橢圓上的第一個點 var firstPoint = center.GetEllipsePoint(radiusX, radiusY, startAngle); startAngle += sectorPart.SpanAngle; // 扇形順時針方向在橢圓上的第二個點 var secondPoint = center.GetEllipsePoint(radiusX, radiusY, startAngle); var sectorFigure = new PathFigure { StartPoint = center }; // 添加中點到第一個點的弦 sectorFigure.Segments.Add(new LineSegment(firstPoint, false)); // 添加第一個點和第二個點之間的弧 sectorFigure.Segments.Add( new ArcSegment(secondPoint, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, false)); var sectorGeometry = new PathGeometry(); sectorGeometry.Figures.Add(sectorFigure); var sectorPath = new Path { Data = sectorGeometry, Fill = sectorPart.FillBrush }; shapes.Add(sectorPath); } var ringShapes = GetRingShapes(center, ringParts); shapes.AddRange(ringShapes); return shapes; } /// <summary> /// 獲取環的形狀列表 /// </summary> /// <param name="center">中心點,為圓表示圓形,為橢圓表示橢圓兩個焦點的中點</param> /// <param name="ringParts">環列表</param> /// <returns>環的形狀列表</returns> private static IEnumerable<Shape> GetRingShapes(Point center, IEnumerable<RingPart> ringParts) { var shapes = new List<Shape>(); foreach (var ringPart in ringParts) { var innerEllipse = new EllipseGeometry(center, ringPart.RadiusX, ringPart.RadiusY); var outterEllipse = new EllipseGeometry(center, ringPart.RadiusX + ringPart.SpanRadiusX, ringPart.RadiusY + ringPart.SpanRadiusY); // 根據里外橢圓求出圓環的形狀 var ringGeometry = new CombinedGeometry(GeometryCombineMode.Xor, innerEllipse, outterEllipse); var ringPath = new Path { Data = ringGeometry, Fill = ringPart.FillBrush }; shapes.Add(ringPath); } return shapes; } } }