WPF 实现扇形统计图

2022-12-06 19:15:23 浏览数 (1)

扇形统计图

原文作者:ArcherSong 博客地址:https://www.cnblogs.com/ganbei/

  • 绘制一个扇形原理也是基于Canvas进行绘制;
  • ArcSegment[1]绘制弧形;
  • 绘制指示线;
  • 绘制文本;
  • 鼠标移入动画;
  • 显示详情Popup
  • 源码Github[2] Gitee[3]

1)SectorChart.cs代码如下;

代码语言:javascript复制
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
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.Animation;
using System.Windows.Media.Effects;
using System.Windows.Shapes;
using WPFDevelopers.Charts.Models;

namespace WPFDevelopers.Charts.Controls
{
    [TemplatePart(Name = CanvasTemplateName, Type = typeof(Canvas))]
    [TemplatePart(Name = PopupTemplateName, Type = typeof(Popup))]
    public class SectorChart : Control
    {
        const string CanvasTemplateName = "PART_Canvas";
        const string PopupTemplateName = "PART_Popup";

        private Canvas _canvas;
        private Popup _popup;
        private double centenrX, centenrY, radius, offsetX, offsetY;
        private Point minPoint;
        private double fontsize = 12;
        private bool flg = false;



        public Brush Fill
        {
            get { return (Brush)GetValue(FillProperty); }
            set { SetValue(FillProperty, value); }
        }

        public static readonly DependencyProperty FillProperty =
            DependencyProperty.Register("Fill", typeof(Brush), typeof(SectorChart), new PropertyMetadata(null));



        public string Text
        {
            get { return (string)GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }

        public static readonly DependencyProperty TextProperty =
            DependencyProperty.Register("Text", typeof(string), typeof(SectorChart), new PropertyMetadata(null));


        public ObservableCollection<PieSerise> ItemsSource
        {
            get { return (ObservableCollection<PieSerise>)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register("ItemsSource", typeof(ObservableCollection<PieSerise>), typeof(SectorChart), new PropertyMetadata(null, new PropertyChangedCallback(ItemsSourceChanged)));

        private static void ItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var view = d as SectorChart;
            if (e.NewValue != null)
                view.DrawArc();
        }

        static SectorChart()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(SectorChart), new FrameworkPropertyMetadata(typeof(SectorChart)));
        }
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            _canvas = GetTemplateChild(CanvasTemplateName) as Canvas;
            _popup = GetTemplateChild(PopupTemplateName) as Popup;
        }

        void DrawArc()
        {
            if (ItemsSource is null || !ItemsSource.Any() || _canvas is null)
                return;
            _canvas.Children.Clear();

            var pieWidth = _canvas.ActualWidth > _canvas.ActualHeight ? _canvas.ActualHeight : _canvas.ActualWidth;
            var pieHeight = _canvas.ActualWidth > _canvas.ActualHeight ? _canvas.ActualHeight : _canvas.ActualWidth;
            centenrX = pieWidth / 2;
            centenrY = pieHeight / 2;
            radius = this.ActualWidth > this.ActualHeight ? this.ActualHeight / 2 : this.ActualWidth / 2;
            double angle = 0;
            double prevAngle = 0;

            var sum = ItemsSource.Select(ser => ser.Percentage).Sum();

            foreach (var item in ItemsSource)
            {
                var line1X = radius * Math.Cos(angle * Math.PI / 180)   centenrX;
                var line1Y = radius * Math.Sin(angle * Math.PI / 180)   centenrY;

                angle = item.Percentage / sum * 360   prevAngle;

                double arcX = 0;
                double arcY = 0;

                if (ItemsSource.Count() == 1 && angle == 360)
                {
                    arcX = centenrX   Math.Cos(359.99999 * Math.PI / 180) * radius;
                    arcY = (radius * Math.Sin(359.99999 * Math.PI / 180))   centenrY;
                }
                else
                {
                    arcX = centenrX   Math.Cos(angle * Math.PI / 180) * radius;
                    arcY = (radius * Math.Sin(angle * Math.PI / 180))   centenrY;
                }


                var line1Segment = new LineSegment(new Point(line1X, line1Y), false);

                bool isLargeArc = item.Percentage / sum > 0.5;


                var arcWidth = radius;
                var arcHeight = radius;
                var arcSegment = new ArcSegment();


                arcSegment.Size = new Size(arcWidth, arcHeight);
                arcSegment.Point = new Point(arcX, arcY);
                arcSegment.SweepDirection = SweepDirection.Clockwise;
                arcSegment.IsLargeArc = isLargeArc;



                var line2Segment = new LineSegment(new Point(centenrX, centenrY), false);


                PieBase piebase = new PieBase();
                piebase.Title = item.Title;
                piebase.Percentage = item.Percentage;
                piebase.PieColor = item.PieColor;
                piebase.LineSegmentStar = line1Segment;
                piebase.ArcSegment = arcSegment;
                piebase.LineSegmentEnd = line2Segment;
                piebase.Angle = item.Percentage / sum * 360;
                piebase.StarPoint = new Point(line1X, line1Y);
                piebase.EndPoint = new Point(arcX, arcY);


                var pathFigure = new PathFigure(new Point(centenrX, centenrY), new List<PathSegment>()
                {
                    line1Segment,
                    arcSegment,
                   line2Segment,
                }, true);



                var pathFigures = new List<PathFigure>()
                {
                    pathFigure,
                };
                var pathGeometry = new PathGeometry(pathFigures);
                var path = new Path() { Fill = item.PieColor, Data = pathGeometry, DataContext = piebase };
                _canvas.Children.Add(path);

                prevAngle = angle;

                var line3 = DrawLine(path);
                if (line3 != null)
                    piebase.Line = line3;
                var textPathGeo = DrawText(path);
                var textpath = new Path() { Fill = item.PieColor, Data = textPathGeo };
                piebase.TextPath = textpath;

                _canvas.Children.Add(textpath);
                path.MouseMove  = Path_MouseMove1;
                path.MouseLeave  = Path_MouseLeave;

                if (ItemsSource.Count() == 1 && angle == 360)
                {
                    _canvas.Children.Add(line3);
                }
                else
                {
                    var outline1 = new Line()
                    {
                        X1 = centenrX,
                        Y1 = centenrY,
                        X2 = line1Segment.Point.X,
                        Y2 = line1Segment.Point.Y,
                        Stroke = Brushes.White,
                        StrokeThickness = 0.8,
                    };
                    var outline2 = new Line()
                    {
                        X1 = centenrX,
                        Y1 = centenrY,
                        X2 = arcSegment.Point.X,
                        Y2 = arcSegment.Point.Y,
                        Stroke = Brushes.White,
                        StrokeThickness = 0.8,
                    };
                    _canvas.Children.Add(outline1);
                    _canvas.Children.Add(outline2);
                    _canvas.Children.Add(line3);
                }

            }
        }
        private void Path_MouseLeave(object sender, MouseEventArgs e)
        {
            _popup.IsOpen = false;
            var path = sender as Path;
            var dt = path.DataContext as PieBase;

            TranslateTransform ttf = new TranslateTransform();
            ttf.X = 0;
            ttf.Y = 0;
            path.RenderTransform = ttf;
            dt.Line.RenderTransform = new TranslateTransform()
            {
                X = 0,
                Y = 0,
            };

            dt.TextPath.RenderTransform = new TranslateTransform()
            {
                X = 0,
                Y = 0,
            };

            path.Effect = new DropShadowEffect()
            {
                Color = (Color)ColorConverter.ConvertFromString("#FF949494"),
                BlurRadius = 20,
                Opacity = 0,
                ShadowDepth = 0
            };
            flg = false;
        }

        private void Path_MouseMove1(object sender, MouseEventArgs e)
        {
            Path path = sender as Path;
            //动画
            if (!flg)
            {

                BegionOffsetAnimation(path);
            }
            ShowMousePopup(path, e);


        }

        void ShowMousePopup(Path path, MouseEventArgs e)
        {
            var data = path.DataContext as PieBase;
            if (!_popup.IsOpen)
                _popup.IsOpen = true;

            var mousePosition = e.GetPosition((UIElement)_canvas.Parent);

            _popup.HorizontalOffset = mousePosition.X   20;
            _popup.VerticalOffset = mousePosition.Y   20;

            Text = (data.Title   " : "   data.Percentage);//显示鼠标当前坐标点
            Fill = data.PieColor;
        }

        void BegionOffsetAnimation(Path path)
        {
            NameScope.SetNameScope(this, new NameScope());
            var pathDataContext = path.DataContext as PieBase;
            var angle = pathDataContext.Angle;

            minPoint = new Point(Math.Round(pathDataContext.StarPoint.X   pathDataContext.EndPoint.X) / 2, Math.Round(pathDataContext.StarPoint.Y   pathDataContext.EndPoint.Y) / 2);


            var v1 = minPoint - new Point(centenrX, centenrY);

            var v2 = new Point(2000, 0) - new Point(0, 0);
            double vAngle = 0;
            if (180 < angle && angle <= 360 && pathDataContext.Percentage / ItemsSource.Select(p => p.Percentage).Sum() >= 0.5)
            {
                vAngle = Math.Round(Vector.AngleBetween(v2, -v1));
            }
            else
            {
                vAngle = Math.Round(Vector.AngleBetween(v2, v1));
            }


            offsetX = 10 * Math.Cos(vAngle * Math.PI / 180);
            offsetY = 10 * Math.Sin(vAngle * Math.PI / 180);

            var line3 = pathDataContext.Line;
            var textPath = pathDataContext.TextPath;

            TranslateTransform LineAnimatedTranslateTransform =
                new TranslateTransform();
            this.RegisterName("LineAnimatedTranslateTransform", LineAnimatedTranslateTransform);
            line3.RenderTransform = LineAnimatedTranslateTransform;


            TranslateTransform animatedTranslateTransform =
                new TranslateTransform();
            this.RegisterName("AnimatedTranslateTransform", animatedTranslateTransform);
            path.RenderTransform = animatedTranslateTransform;

            TranslateTransform TextAnimatedTranslateTransform =
               new TranslateTransform();
            this.RegisterName("TextAnimatedTranslateTransform", animatedTranslateTransform);
            textPath.RenderTransform = animatedTranslateTransform;


            DoubleAnimation daX = new DoubleAnimation();
            Storyboard.SetTargetProperty(daX, new PropertyPath(TranslateTransform.XProperty));
            daX.Duration = new Duration(TimeSpan.FromSeconds(0.2));
            daX.From = 0;
            daX.To = offsetX;


            DoubleAnimation daY = new DoubleAnimation();

            Storyboard.SetTargetName(daY, nameof(animatedTranslateTransform));
            Storyboard.SetTargetProperty(daY, new PropertyPath(TranslateTransform.YProperty));
            daY.Duration = new Duration(TimeSpan.FromSeconds(0.2));
            daY.From = 0;
            daY.To = offsetY;

            path.Effect = new DropShadowEffect()
            {
                Color = (Color)ColorConverter.ConvertFromString("#2E2E2E"),
                BlurRadius = 33,
                Opacity = 0.6,
                ShadowDepth = 0
            };

            animatedTranslateTransform.BeginAnimation(TranslateTransform.XProperty, daX);
            animatedTranslateTransform.BeginAnimation(TranslateTransform.YProperty, daY);
            LineAnimatedTranslateTransform.BeginAnimation(TranslateTransform.XProperty, daX);
            LineAnimatedTranslateTransform.BeginAnimation(TranslateTransform.YProperty, daY);
            TextAnimatedTranslateTransform.BeginAnimation(TranslateTransform.XProperty, daX);
            TextAnimatedTranslateTransform.BeginAnimation(TranslateTransform.YProperty, daY);




            flg = true;
        }
        /// <summary>
        /// 画指示线
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        Polyline DrawLine(Path path)
        {
            NameScope.SetNameScope(this, new NameScope());
            var pathDataContext = path.DataContext as PieBase;
            var angle = pathDataContext.Angle;
            pathDataContext.Line = null;
            minPoint = new Point(Math.Round(pathDataContext.StarPoint.X   pathDataContext.EndPoint.X) / 2, Math.Round(pathDataContext.StarPoint.Y   pathDataContext.EndPoint.Y) / 2);

            Vector v1;
            if (angle > 180 && angle < 360)
            {
                v1 = new Point(centenrX, centenrY) - minPoint;
            }
            else if (angle == 180 || angle == 360)
            {
                if (Math.Round(pathDataContext.StarPoint.X) == Math.Round(pathDataContext.EndPoint.X))
                {
                    v1 = new Point(radius * 2, radius) - new Point(centenrX, centenrY);

                }
                else
                {
                    if (Math.Round(pathDataContext.StarPoint.X) - Math.Round(pathDataContext.EndPoint.X) == 2 * radius)
                    {
                        v1 = new Point(radius, 2 * radius) - new Point(centenrX, centenrY);
                    }
                    else
                    {
                        v1 = new Point(radius, 0) - new Point(centenrX, centenrY);
                    }
                }
            }
            else
            {
                v1 = minPoint - new Point(centenrX, centenrY);
            }
            v1.Normalize();
            var Vmin = v1 * radius;
            var RadiusToNodal = Vmin   new Point(centenrX, centenrY);
            var v2 = new Point(2000, 0) - new Point(0, 0);
            double vAngle = 0;
            vAngle = Math.Round(Vector.AngleBetween(v2, v1));

            offsetX = 10 * Math.Cos(vAngle * Math.PI / 180);
            offsetY = 10 * Math.Sin(vAngle * Math.PI / 180);

            var prolongPoint = new Point(RadiusToNodal.X   offsetX * 1, RadiusToNodal.Y   offsetY * 1);

            if (RadiusToNodal.X == double.NaN || RadiusToNodal.Y == double.NaN || prolongPoint.X == double.NaN || prolongPoint.Y == double.NaN)
                return null;


            var point1 = RadiusToNodal;
            var point2 = prolongPoint;
            Point point3;
            if (prolongPoint.X >= radius)
                point3 = new Point(prolongPoint.X   10, prolongPoint.Y);
            else
                point3 = new Point(prolongPoint.X - 10, prolongPoint.Y);
            PointCollection polygonPoints = new PointCollection();
            polygonPoints.Add(point1);
            polygonPoints.Add(point2);
            polygonPoints.Add(point3);
            var line3 = new Polyline();
            line3.Points = polygonPoints;
            line3.Stroke = pathDataContext.PieColor;
            pathDataContext.PolylineEndPoint = point3;

            return line3;
        }

        PathGeometry DrawText(Path path)
        {
            NameScope.SetNameScope(this, new NameScope());
            var pathDataContext = path.DataContext as PieBase;

            Typeface typeface = new Typeface
                (new FontFamily("Microsoft YaHei"),
                FontStyles.Normal,
                FontWeights.Normal, FontStretches.Normal);

            FormattedText text = new FormattedText(
                pathDataContext.Title,
                new System.Globalization.CultureInfo("zh-cn"),
                FlowDirection.LeftToRight, typeface, fontsize, Brushes.RosyBrown
                );

            var textWidth = text.Width;

            Geometry geo = null;
            if (pathDataContext.PolylineEndPoint.X > radius)
                geo = text.BuildGeometry(new Point(pathDataContext.PolylineEndPoint.X   4, pathDataContext.PolylineEndPoint.Y - fontsize / 1.8));
            else
                geo = text.BuildGeometry(new Point(pathDataContext.PolylineEndPoint.X - textWidth - 4, pathDataContext.PolylineEndPoint.Y - fontsize / 1.8));
            PathGeometry pathGeometry = geo.GetFlattenedPathGeometry();
            return pathGeometry;

        }
    }
}

2)SectorChart.xaml 代码如下;

代码语言:javascript复制
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:controls="clr-namespace:WPFDevelopers.Charts.Controls">
    <Style TargetType="{x:Type controls:SectorChart}">
        <Setter Property="Width" Value="300"/>
        <Setter Property="Height" Value="300"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type controls:SectorChart}">
                    <Grid>
                        <Popup x:Name="PART_Popup" 
                               IsOpen="False"
                               Placement="Relative" 
                               AllowsTransparency="True">
                            <Border Background="White" 
                                    CornerRadius="5" 
                                    Padding="14"
                                    BorderThickness="0"
                                    BorderBrush="Transparent">
                                <StackPanel >
                                    <Ellipse Width="20" Height="20"
                                             Fill="{TemplateBinding Fill}"/>
                                    <TextBlock Background="White" 
                                               Padding="9,4,9,4" TextWrapping="Wrap" 
                                               Text="{TemplateBinding Text}"/>
                                </StackPanel>
                            </Border>
                        </Popup>

                        <Canvas x:Name="PART_Canvas"  HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                Width="{TemplateBinding ActualWidth}"
                                Height="{TemplateBinding ActualHeight}">
                        </Canvas>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

3) MainWindow.xaml使用如下;

代码语言:javascript复制
 xmlns:wsCharts="https://github.com/WPFDevelopersOrg.WPFDevelopers.Charts"

<wsCharts:SectorChart  ItemsSource="{Binding ItemsSource,RelativeSource={RelativeSource AncestorType=local:MainWindow}}"
                                  Margin="30" />

4) MainWindow.xaml.cs代码如下;

代码语言:javascript复制
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Media;
using WPFDevelopers.Charts.Models;

namespace WPFDevelopers.Charts.Samples
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow 
    {
        public ObservableCollection<PieSerise> ItemsSource
        {
            get { return (ObservableCollection<PieSerise>)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register("ItemsSource", typeof(ObservableCollection<PieSerise>), typeof(MainWindow), new PropertyMetadata(null));

        public MainWindow()
        {
            InitializeComponent();
            Loaded  = MainWindow_Loaded;
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            ItemsSource = new ObservableCollection<PieSerise>();
            var collection1 = new ObservableCollection<PieSerise>();
            collection1.Add(new PieSerise
            {
                Title = "2012",
                Percentage = 30,
                PieColor = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#5B9BD5")),
            });
            collection1.Add(
                new PieSerise
                {
                    Title = "2013",
                    Percentage = 140,
                    PieColor = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4472C4")),
                });

            collection1.Add(new PieSerise
            {
                Title = "2014",
                Percentage = 49,
                PieColor = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#007fff")),
            });

            collection1.Add(new PieSerise
            {
                Title = "2015",
                Percentage = 50,
                PieColor = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#ED7D31")),
            });
            collection1.Add(new PieSerise
            {
                Title = "2016",
                Percentage = 30,
                PieColor = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FFC000")),
            });

            collection1.Add(new PieSerise
            {
                Title = "2017",
                Percentage = 30,
                PieColor = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#ff033e")),
            });
            ItemsSource = collection1;
        }
    }
}

参考资料

[1] ArcSegment: https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.media.arcsegment?view=windowsdesktop-6.0

[2] Github: https://github.com/WPFDevelopersOrg/WPFDevelopers.Charts

[3] Gitee: https://gitee.com/WPFDevelopersOrg/WPFDevelopers.Charts

0 人点赞