WPF实现的简单饼图

简介

前段时间帮一个同事的忙,利用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;
        }
    }
}
时间: 2024-10-14 07:17:27

WPF实现的简单饼图的相关文章

WPF 3D:简单的Point3D和Vector3D动画创造一个旋转的正方体

原文:WPF 3D:简单的Point3D和Vector3D动画创造一个旋转的正方体 运行结果: 事实上很简单,定义好一个正方体,处理好纹理.关于MeshGeometry3D的正确定义和纹理这里就不多讲了,可以参考我以前写过的一些文章: WPF 3D: MeshGeometry3D纹理坐标的正确定义 WPF 3D:MeshGeometry3D的定义和光照 接下来就是怎样让它动起来.我们通过3D点动画来改变照相机(Camera类型)的位置(Position属性)从而使正方体动起来(这样的话实际上正方

WPF动画制作简单的按钮动画

主界面的代码 <StackPanel ButtonBase.Click="Grid_Click"> <Button Content="逐渐变大缩小"/> <Button Content="鼠标移动特效" /> </StackPanel> cs : //这事件不做过多的解释有基础的一看就会明白 private void Grid_Click(object sender, RoutedEventArgs

WPF中的简单水动画

原文 https://stuff.seans.com/2008/08/21/simple-water-animation-in-wpf/ 很多年前(80年代中期),我在一家拥有Silicon Graphics工作站的公司工作.在旨在展示SGI机器高端图形的少数演示中,有一个模拟了一个小线框网格中的波传播.通过更改网格中的点的高度然后让模拟运行来玩游戏非常有趣.并且SGI机器足够快,结果动画只是令人着迷. 在WPF中重新创建这个水模拟似乎是一个很好的方式来学习WPF中的3D图形.(最终结果在这里)

WPF框架MVVM简单例子

MVVM是Model-View-ViewModel的缩写形式,它通常被用于WPF或Silverlight开发.Model——可以理解为带有字段,属性的类.View——可以理解为我们所看到的UI.View Model在View和Model之间,起到连接的作用,并且使得View和Model层分离.View Model不仅仅是Model的包装,它还包含了程序逻辑,以及Model扩展,例如,如果Model中有一个公开属性不需要在UI上显示,此时我们可以不再View Model中去定义它. 在MVVM中,

WPF单线程定时器 简单实例

//窗体加载完毕 void MyMessageBox_Loaded(object sender, RoutedEventArgs e) { //启动定时期倒计时,多线程计时 //System.Threading.Timer timer: //启动单线程计时 System.Windows.Threading.DispatcherTimer timer = new System.Windows.Threading.DispatcherTimer(); timer.Interval = new Tim

用ggplot包画一个简单饼图

首先用library函数加载ggplot2包 library(ggplot2) library(dplyr) library(tidyr) library(splines) 接下来,进行数据准备: df <- data.frame( var=LETTERS[1:3], id=1:3, a=c(0.25,0,35,0,4), stringsAsFactors = F #不转换为因子 ) 我们已经有了一个一维数组,而data.frame是将这个数组转换为二维,print的结果是这样的: var id

WPF的ComboBox简单用法

1. ComboBox:下拉列表框 效果如下: 2.通常用法是 显示内容 + 选中内容后获得的值(也就是 Name = Value的键值对) 故以键值对来定义一个类,如: public class CategoryInfo { public string Name { get; set;} public string Value{ get; set;} } 3. 再使用 List来存储这些选项的集合 List<CategoryInfo> categoryList = new List<C

wpf BackgroundWorker 的简单用法

1. 登陆中.. 登陆成功 BackgroundWorker worker = new BackgroundWorker(); worker.DoWork += (o, ea) => { Login(sName, sPassword); }; worker.RunWorkerCompleted += (o, ea) => { if (ea.Error != null) { tbMsg.Text = ea.Error.Message; Password.Clear(); Password.Foc

WPF C# 命令 学习

1.概述 1.1 WPF C# 命令的本质 命令是 WPF 中的输入机制,它提供的输入处理比设备输入具有更高的语义级别. 例如,在许多应用程序中都能找到的“复制”.“剪切”和“粘贴”操作就是命令. WPF 中的命令是通过实现 ICommand 接口创建的. ICommand 的 WPF 实现是 RoutedCommand 类,这是WPF C# 命令的本质. 1.2 WPF C# 命令的机制 1.2.1 编程范围 ICommand 公开两个方法(Execute 及 CanExecute)和一个事件