WPF 实现饼状统计图

214 阅读3分钟

WPF 实现饼状统计图

  • 框架支持.NET 4 至 .NET 8;

  • Visual Studio 2022;

ChartPie 详解

新增依赖属性 Datas 存储饼图的数据,当数据发生更改时触发控件的重绘。

构造初始化颜色组 (vibrantColors) 为了区分每个扇形区显示不同的颜色。

绘制饼图

var drawingPen = CreatePen(2);  
var boldDrawingPen = CreatePen(4);  
var pieWidth = ActualWidth > ActualHeight ? ActualHeight : ActualWidth;  
var pieHeight = ActualWidth > ActualHeight ? ActualHeight : ActualWidth;  
centerX = pieWidth / 2;  
centerY = pieHeight / 2;  
radius = ActualWidth > ActualHeight ? ActualHeight / 2 : ActualWidth / 2;  
  • 计算饼图的宽度和高度,以确保饼图是圆形的。

  • 计算圆心与半径。

绘制每个扇形

var angle = 0d;  
var prevAngle = 0d;  
var sum = Datas.Select(ser => ser.Value).Sum();  
var index = 0;  
var isFirst = false;  
foreach (var item in Datas)  
{  
    // 计算起始和结束角度  
    var arcStartX = radius * Math.Cos(angle * Math.PI / 180) + centerX;  
    var arcStartY = radius * Math.Sin(angle * Math.PI / 180) + centerY;  
    angle = item.Value / sum * 360 + prevAngle;  
    var arcEndX = 0d;  
    var arcEndY = 0d;  
    if (Datas.Count() == 1 && angle == 360)  
    {  
        isFirst = true;  
        arcEndX = centerX + Math.Cos(359.99999 * Math.PI / 180) * radius;  
        arcEndY = radius * Math.Sin(359.99999 * Math.PI / 180) + centerY;  
    }  
    else  
    {  
        arcEndX = centerX + Math.Cos(angle * Math.PI / 180) * radius;  
        arcEndY = radius * Math.Sin(angle * Math.PI / 180) + centerY;  
    }  
  
    var startPoint = new Point(arcStartX, arcStartY);  
    var line1Segment = new LineSegment(startPoint, false);  
    var isLargeArc = item.Value / sum > 0.5;  
    var arcSegment = new ArcSegment  
    {  
        Size = new Size(radius, radius),  
        Point = new Point(arcEndX, arcEndY),  
        SweepDirection = SweepDirection.Clockwise,  
        IsLargeArc = isLargeArc  
    };  
    var center = new Point(centerX, centerY);  
    var line2Segment = new LineSegment(center, false);  
    var pathGeometry = new PathGeometry(new[]  
    {  
        new PathFigure(center, new List<PathSegment>  
        {  
            line1Segment,  
            arcSegment,  
            line2Segment  
        }, true)  
    });  
  
    pathGeometries.Add(pathGeometry,  
        $"{item.Key} : {item.Value.FormatNumber()}");  
  
    var backgroupBrush = new SolidColorBrush  
    {  
        Color = vibrantColors[  
            index >= vibrantColors.Length  
                ? index % vibrantColors.Length  
                : index]  
    };  
    backgroupBrush.Freeze();  
    drawingContext.DrawGeometry(backgroupBrush, null, pathGeometry);  
  
    index++;  
    if (!isFirst)  
    {  
        if (index == 1)  
            drawingContext.DrawLine(boldDrawingPen, center, startPoint);  
        else  
            drawingContext.DrawLine(drawingPen, center, startPoint);  
    }  
    prevAngle = angle;  
}  
  • 初始化角度 angleprevAngle,计算数据总和(sum)。

  • 循环 Datas 集合,计算每条数据所需占的扇形区的起始角度和结束的角度。

  • 如果只有一条数据那么角度为 360度,然后绘制圆形。

  • 使用 ArcSegment 绘制圆形的弧度,连接圆心和扇形区边缘。

  • 将生成的 PathGeometry 添加到 pathGeometries 中,并绘制每个的扇形区。

  • 绘制每个扇形区的边框,根据索引设置画笔的宽度用于边框。

  • 更新 prevAngle 以用于计算下一个扇形区的角度。

1)新增 ChartPie.cs 代码如下:

using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Windows;  
using System.Windows.Controls;  
using System.Windows.Controls.Primitives;  
using System.Windows.Input;  
using System.Windows.Media;  
using System.Windows.Media.Effects;  
using System.Windows.Shapes;  
using WPFDevelopers.Core;  
  
namespace WPFDevelopers.Controls  
{  
    public class ChartPie : Control  
    {  
        public static readonly DependencyProperty DatasProperty =  
            DependencyProperty.Register("Datas"typeof(IEnumerable<KeyValuePair<stringdouble>>),  
                typeof(ChartPie), new UIPropertyMetadata(DatasChanged));  
  
        private Border _border;  
        private Ellipse _ellipse;  
        private KeyValuePair<PathGeometry, string> _lastItem;  
        private Popup _popup;  
        private StackPanel _stackPanel;  
        private TextBlock _textBlock;  
        private double centerX, centerY, radius;  
        private bool isPopupOpen;  
        private readonly Dictionary<PathGeometry, string> pathGeometries = new Dictionary<PathGeometry, string>();  
  
        private readonly Color[] vibrantColors;  
  
        public ChartPie()  
        {  
            vibrantColors = new[]  
            {  
                Color.FromArgb(25584112198),  
                Color.FromArgb(255145204117),  
                Color.FromArgb(25525020088),  
                Color.FromArgb(255238102102),  
                Color.FromArgb(255115192222),  
                Color.FromArgb(25559162114),  
                Color.FromArgb(25525213282),  
                Color.FromArgb(25515496180),  
                Color.FromArgb(255234124204)  
            };  
        }  
  
        public IEnumerable<KeyValuePair<stringdouble>> Datas  
        {  
            get => (IEnumerable<KeyValuePair<stringdouble>>) GetValue(DatasProperty);  
            set => SetValue(DatasProperty, value);  
        }  
  
        private static void DatasChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)  
        {  
            var ctrl = d as ChartPie;  
            if (e.NewValue != null)  
                ctrl.InvalidateVisual();  
        }  
  
        protected override void OnMouseMove(MouseEventArgs e)  
        {  
            base.OnMouseMove(e);  
            if (Datas == null || Datas.Count() == 0 || isPopupOpen) return;  
            if (_popup == null)  
            {  
                _popup = new Popup  
                {  
                    AllowsTransparency = true,  
                    Placement = PlacementMode.MousePoint,  
                    PlacementTarget = this,  
                    StaysOpen = false  
                };  
                _popup.MouseMove += (y, j) =>  
                {  
                    var point = j.GetPosition(this);  
                    if (isPopupOpen && _lastItem.Value != null)  
                        if (!IsMouseOverGeometry(_lastItem.Key))  
                        {  
                            _popup.IsOpen = false;  
                            isPopupOpen = false;  
                            _lastItem = new KeyValuePair<PathGeometry, string>();  
                        }  
                };  
                _popup.Closed += delegate { isPopupOpen = false; };  
  
                _textBlock = new TextBlock  
                {  
                    HorizontalAlignment = HorizontalAlignment.Center,  
                    VerticalAlignment = VerticalAlignment.Center,  
                    Foreground = (Brush) Application.Current.TryFindResource("WD.WindowForegroundColorBrush"),  
                    Padding = new Thickness(4020)  
                };  
                _ellipse = new Ellipse  
                {  
                    Width = 10,  
                    Height = 10,  
                    Stroke = Brushes.White  
                };  
                _stackPanel = new StackPanel {Orientation = Orientation.Horizontal};  
                _stackPanel.Children.Add(_ellipse);  
                _stackPanel.Children.Add(_textBlock);  
  
                _border = new Border  
                {  
                    Child = _stackPanel,  
                    Background = (Brush) Application.Current.TryFindResource("WD.ChartFillSolidColorBrush"),  
                    Effect = Application.Current.TryFindResource("WD.PopupShadowDepth"as DropShadowEffect,  
                    Margin = new Thickness(10),  
                    CornerRadius = new CornerRadius(3),  
                    Padding = new Thickness(6)  
                };  
                _popup.Child = _border;  
            }  
  
            var index = 0;  
            foreach (var pathGeometry in pathGeometries)  
            {  
                if (IsMouseOverGeometry(pathGeometry.Key))  
                {  
                    isPopupOpen = true;  
                    _ellipse.Fill = new SolidColorBrush  
                    {  
                        Color = vibrantColors[index >= vibrantColors.Length ? index % vibrantColors.Length : index]  
                    };  
                    _textBlock.Text = pathGeometry.Value;  
                    _popup.IsOpen = true;  
                    _lastItem = pathGeometry;  
                    break;  
                }  
  
                index++;  
            }  
        }  
  
        private bool IsMouseOverGeometry(PathGeometry pathGeometry)  
        {  
            var mousePosition = Mouse.GetPosition(this);  
            return pathGeometry.FillContains(mousePosition);  
        }  
  
        protected override void OnRender(DrawingContext drawingContext)  
        {  
            base.OnRender(drawingContext);  
            if (Datas == null || Datas.Count() == 0)  
                return;  
            SnapsToDevicePixels = true;  
            UseLayoutRounding = true;  
            pathGeometries.Clear();  
            var drawingPen = CreatePen(2);  
            var boldDrawingPen = CreatePen(4);  
            var pieWidth = ActualWidth > ActualHeight ? ActualHeight : ActualWidth;  
            var pieHeight = ActualWidth > ActualHeight ? ActualHeight : ActualWidth;  
            centerX = pieWidth / 2;  
            centerY = pieHeight / 2;  
            radius = ActualWidth > ActualHeight ? ActualHeight / 2 : ActualWidth / 2;  
            var angle = 0d;  
            var prevAngle = 0d;  
            var sum = Datas.Select(ser => ser.Value).Sum();  
            var index = 0;  
            var isFirst = false;  
            foreach (var item in Datas)  
            {  
                var arcStartX = radius * Math.Cos(angle * Math.PI / 180) + centerX;  
                var arcStartY = radius * Math.Sin(angle * Math.PI / 180) + centerY;  
                angle = item.Value / sum * 360 + prevAngle;  
                var arcEndX = 0d;  
                var arcEndY = 0d;  
                if (Datas.Count() == 1 && angle == 360)  
                {  
                    isFirst = true;  
                    arcEndX = centerX + Math.Cos(359.99999 * Math.PI / 180) * radius;  
                    arcEndY = radius * Math.Sin(359.99999 * Math.PI / 180) + centerY;  
                }  
                else  
                {  
                    arcEndX = centerX + Math.Cos(angle * Math.PI / 180) * radius;  
                    arcEndY = radius * Math.Sin(angle * Math.PI / 180) + centerY;  
                }  
  
                var startPoint = new Point(arcStartX, arcStartY);  
                var line1Segment = new LineSegment(startPoint, false);  
                var isLargeArc = item.Value / sum > 0.5;  
                var arcSegment = new ArcSegment();  
                var size = new Size(radius, radius);  
                var endPoint = new Point(arcEndX, arcEndY);  
                arcSegment.Size = size;  
                arcSegment.Point = endPoint;  
                arcSegment.SweepDirection = SweepDirection.Clockwise;  
                arcSegment.IsLargeArc = isLargeArc;  
                var center = new Point(centerX, centerY);  
                var line2Segment = new LineSegment(center, false);  
  
                var pathGeometry = new PathGeometry(new[]  
                {  
                    new PathFigure(new Point(centerX, centerY), new List<PathSegment>  
                    {  
                        line1Segment,  
                        arcSegment,  
                        line2Segment  
                    }, true)  
                });  
                pathGeometries.Add(pathGeometry,  
                    $"{item.Key} : {item.Value.FormatNumber()}");  
                var backgroupBrush = new SolidColorBrush  
                {  
                    Color = vibrantColors[  
                        index >= vibrantColors.Length  
                            ? index % vibrantColors.Length  
                            : index]  
                };  
                backgroupBrush.Freeze();  
  
                drawingContext.DrawGeometry(backgroupBrush, null, pathGeometry);  
                index++;  
                if (!isFirst)  
                {  
                    if (index == 1)  
                        drawingContext.DrawLine(boldDrawingPen, center, startPoint);  
                    else  
                        drawingContext.DrawLine(drawingPen, center, startPoint);  
                }  
  
                prevAngle = angle;  
            }  
        }  
  
        private Pen CreatePen(double thickness)  
        {  
            var pen = new Pen  
            {  
                Thickness = thickness,  
                Brush = Brushes.White  
            };  
            pen.Freeze();  
            return pen;  
        }  
    }  
}  
`

2)新增 `ChartPieExample.xaml` 示例代码如下:

`        <Grid Background="{DynamicResource WD.BackgroundSolidColorBrush}">  
            <Grid.RowDefinitions>  
                <RowDefinition />  
                <RowDefinition Height="Auto" />  
            </Grid.RowDefinitions>  
            <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">  
                <Border  
                    Height="300"  
                    Margin="30,0"  
                    Background="{DynamicResource WD.BackgroundSolidColorBrush}">  
                    <wd:ChartPie Datas="{Binding Datas, RelativeSource={RelativeSource AncestorType=local:ChartPieExample}}" />  
                </Border>  
            </ScrollViewer>  
            <Button  
                Grid.Row="1"  
                Width="200"  
                VerticalAlignment="Bottom"  
                Click="Button_Click"  
                Content="刷新"  
                Style="{StaticResource WD.PrimaryButton}" />  
        </Grid>  

3)新增 ChartPieExample.xaml.cs 示例代码如下:

using System.Collections.Generic;  
using System.Linq;  
using System.Windows;  
using System.Windows.Controls;  
  
namespace WPFDevelopers.Samples.ExampleViews  
{  
    /// <summary>  
    /// ChartPieExample.xaml 的交互逻辑  
    /// </summary>  
    public partial class ChartPieExample : UserControl  
    {  
        public IEnumerable<KeyValuePair<stringdouble>> Datas  
        {  
            get { return (IEnumerable<KeyValuePair<stringdouble>>)GetValue(DatasProperty); }  
            set { SetValue(DatasProperty, value); }  
        }  
  
        public static readonly DependencyProperty DatasProperty =  
            DependencyProperty.Register("Datas"typeof(IEnumerable<KeyValuePair<stringdouble>>), typeof(ChartPieExample), new PropertyMetadata(null));  
  
        private Dictionary<string, IEnumerable<KeyValuePair<stringdouble>>> keyValues = new Dictionary<string, IEnumerable<KeyValuePair<stringdouble>>>();  
        private int _index = 0;  
        public ChartPieExample()  
        {  
            InitializeComponent();  
            var models1 = new[]  
            {  
                new KeyValuePair<stringdouble>("Mon"120),  
                new KeyValuePair<stringdouble>("Tue"530),  
                new KeyValuePair<stringdouble>("Wed"1060),  
                new KeyValuePair<stringdouble>("Thu"140),  
                new KeyValuePair<stringdouble>("Fri"8000.123456) ,  
                new KeyValuePair<stringdouble>("Sat"200) ,  
                new KeyValuePair<stringdouble>("Sun"300) ,  
            };  
            var models2 = new[]  
            {  
                new KeyValuePair<stringdouble>("Bing"120),  
                new KeyValuePair<stringdouble>("Google"170),  
                new KeyValuePair<stringdouble>("Baidu"30),  
                new KeyValuePair<stringdouble>("Github"200),  
                new KeyValuePair<stringdouble>("Stack Overflow"100) ,  
                new KeyValuePair<stringdouble>("Runoob"180) ,  
                new KeyValuePair<stringdouble>("Open AI"90) ,  
                new KeyValuePair<stringdouble>("Open AI2"93) ,  
                new KeyValuePair<stringdouble>("Open AI3"94) ,  
                new KeyValuePair<stringdouble>("Open AI4"95) ,  
            };  
            keyValues.Add("1", models1);  
            keyValues.Add("2", models2);  
            Datas = models1;  
        }  
  
        private void Button_Click(object sender, RoutedEventArgs e)  
        {  
            _index++;  
            if (_index >= keyValues.Count)  
            {  
                _index = 0;  
            }  
            Datas = keyValues.ToList()[_index].Value;  
        }  
    }  
}  

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!

控件名:ChartPie

作   者:WPFDevelopersOrg - 驚鏵&ArcherSong

原文链接[1]:github.com/WPFDevelope…

码云链接[2]:gitee.com/WPFDevelope…

声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!