WPF实现绘制扇形统计图的示例代码
作者:ArcherSong
这篇文章主要介绍了如何利用WPF绘制扇形统计图,文中的示例代码讲解详细,对我们学习或工作有一定帮助,感兴趣的小伙伴可以了解一下
扇形统计图
- 绘制一个扇形原理也是基于
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); 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 代码如下;
<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; } } }
以上就是WPF实现绘制扇形统计图的示例代码的详细内容,更多关于WPF扇形统计图的资料请关注脚本之家其它相关文章!