扇形統計圖
原文作者:ArcherSong
博客地址:https://www.cnblogs.com/ganbei/
繪制一個扇形原理也是基于
Canvas
進行繪制;ArcSegment[1]繪制弧形;
繪制指示線;
繪制文本;
鼠標移入動畫;
顯示詳情
Popup
;源碼Github[2]Gitee[3]

1)SectorChart.cs代碼如下;
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);elsepoint3?=?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));elsegeo?=?text.BuildGeometry(new?Point(pathDataContext.PolylineEndPoint.X?-?textWidth?-?4,?pathDataContext.PolylineEndPoint.Y?-?fontsize?/?1.8));PathGeometry?pathGeometry?=?geo.GetFlattenedPathGeometry();return?pathGeometry;}}
}
2)SectorChart.xaml 代碼如下;
<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使用如下;
xmlns:wsCharts="https://github.com/WPFDevelopersOrg.WPFDevelopers.Charts"<wsCharts:SectorChart??ItemsSource="{Binding?ItemsSource,RelativeSource={RelativeSource?AncestorType=local:MainWindow}}"Margin="30"?/>
4) MainWindow.xaml.cs代碼如下;
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