WPF 实现雷达图

 WPF 实现雷达图

控件名:ChartRadar

作   者:WPFDevelopersOrg - 驚鏵

原文链接[1]:https://github.com/WPFDevelopersOrg/WPFDevelopers

码云链接[2]:https://gitee.com/WPFDevelopersOrg/WPFDevelopers

  • 框架支持.NET4 至 .NET8

  • Visual Studio 2022;

接着上一篇

雷达图是一种显示数据点及其之间变化的方式。

f78513d2a5b796fa01a0923e6ca93f75.png

1)修改 ChartRadar 代码如下:

  • ChartRadar 类继承自上一篇的 ChartBase 只是为了使用 Datas 与一些属性和重写了 OnRender 方法,用于在控件上绘制雷达图。

  • OnRender 方法首先检查 Datas 是否存在,如果没有数据则直接返回。然后设置一些绘图相关的属性,并根据数据绘制雷达图的各个点和连接线。

  • DrawPoints 方法用于绘制雷达图的多边形轮廓。它接受圆的半径和绘图上下文作为参数。通过调用 GetPolygonPoint 方法获取多边形的顶点,并使用 StreamGeometry 对象绘制多边形。

  • GetPolygonPoint 方法用于计算多边形的顶点坐标。接受中心点、半径和绘图上下文(可选)作为参数。通过遍历数据项计算每个顶点的坐标,并根据需要在顶点处绘制文本。最后返回顶点坐标集合。

  • OnRender 方法:

    • 计算数据项在雷达图中对应的点坐标,并将其添加到 points 集合中。

    • 根据计算出的点坐标创建一个矩形 rect,并将其添加到 rects 集合中。

    • 创建一个稍微扩大的矩形 nRect,并将其与对应的数据项信息添加到 dicts 字典中。

    • 创建一个 StreamGeometry 对象,用于定义要绘制的几何图形路径。

    • 在使用 StreamGeometry 之前,通过 streamGeometry.Open() 方法获取一个 StreamGeometryContext,以便开始定义图形路径。

    • 遍历数据集 Datas,对每个数据项执行以下操作:

    • 使用 geometryContext.BeginFiguregeometryContext.PolyLineTo 方法来绘制多边形,连接 points 中的点。

    • 冻结 streamGeometry,以便提高性能和安全性。

    • 创建一个填充颜色为主题色的矩形 rectBrush,并设置透明度为0.5

    • 使用 DrawingContext.DrawGeometry 方法将雷达图的几何路径填充为指定的颜色,使用 myPen 对象定义边界线条。

    • 创建一个画笔 drawingPen,用于绘制每个数据点的边界线条,生成笔画的粗细值为2,笔刷为预定义的 NormalBrush

    • 创建一个背景色刷子 backgroupBrush,颜色为预定义的背景色。

    • 遍历 rects 集合中的每个矩形,并使用 DrawingContext.DrawGeometry 方法绘制每个点位的圆。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using WPFDevelopers.Helpers;

namespace WPFDevelopers.Controls
{
    public class ChartRadar : ChartBase
    {
        private PointCollection _points;
        private double _h, _w;
        static ChartRadar()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(ChartRadar),
                new FrameworkPropertyMetadata(typeof(ChartRadar)));
        }
        protected override void OnRender(DrawingContext drawingContext)
        {
            if (Datas == null || Datas.Count() == 0)
                return;
            SnapsToDevicePixels = true;
            UseLayoutRounding = true;
            var dicts = new Dictionary<Rect, string>();
            var rects = new List<Rect>();
            var max = Convert.ToInt32(Datas.Max(kvp => kvp.Value)) + 50;
            double v = StartX;
            for (var i = 0; i < Rows; i++)
            {
                DrawPoints(v, drawingContext, i == Rows - 1);
                v += StartX;
            }

            var myPen = new Pen
            {
                Thickness = 3,
                Brush = NormalBrush
            };
            myPen.Freeze();

            var streamGeometry = new StreamGeometry();
            using (var geometryContext = streamGeometry.Open())
            {
                var points = new PointCollection();
                short index = 0;
                foreach (var item in Datas)
                {
                    if (index < _points.Count)
                    {
                        var startPoint = _points[index];
                        var point = new Point((startPoint.X - _w) / max * item.Value + _w,
                        (startPoint.Y - _h) / max * item.Value + _h);
                        points.Add(point);
                        var ellipsePoint = new Point(point.X - EllipseSize / 2, point.Y - EllipseSize / 2);
                        var rect = new Rect(ellipsePoint, new Size(EllipseSize, EllipseSize));
                        rects.Add(rect);
                        var nRect = new Rect(rect.Left - EllipsePadding, rect.Top - EllipsePadding, rect.Width + EllipsePadding, rect.Height + EllipsePadding);
                        dicts.Add(nRect, $"{item.Key} : {item.Value}");
                    }
                    index++;
                }
                geometryContext.BeginFigure(points[points.Count - 1], true, true);
                geometryContext.PolyLineTo(points, true, true);
            }
            PointCache = dicts;
            streamGeometry.Freeze();
            var color = (Color)Application.Current.TryFindResource("WD.PrimaryNormalColor");
            var rectBrush = new SolidColorBrush(color);
            rectBrush.Opacity = 0.5;
            rectBrush.Freeze();
            drawingContext.DrawGeometry(rectBrush, myPen, streamGeometry);

            var drawingPen = new Pen
            {
                Thickness = 2,
                Brush = NormalBrush
            };
            drawingPen.Freeze();

            var backgroupBrush = new SolidColorBrush()
            {
                Color = (Color)Application.Current.TryFindResource("WD.BackgroundColor")
            };
            backgroupBrush.Freeze();
            foreach (var item in rects)
            {
                var ellipseGeom = new EllipseGeometry(item);
                drawingContext.DrawGeometry(backgroupBrush, drawingPen, ellipseGeom);
            }
        }
        private void DrawPoints(double circleRadius, DrawingContext drawingContext, bool isDrawText = false)
        {
            var myPen = new Pen
            {
                Thickness = 1,
                Brush = Application.Current.TryFindResource("WD.ChartXAxisSolidColorBrush") as Brush
            };
            myPen.Freeze();
            var streamGeometry = new StreamGeometry();
            using (var geometryContext = streamGeometry.Open())
            {
                _h = ActualHeight / 2;
                _w = ActualWidth / 2;
                if (isDrawText)
                    _points = GetPolygonPoint(new Point(_w, _h), circleRadius, drawingContext);
                else
                    _points = GetPolygonPoint(new Point(_w, _h), circleRadius);
                geometryContext.BeginFigure(_points[_points.Count - 1], true, true);
                geometryContext.PolyLineTo(_points, true, true);
            }
            streamGeometry.Freeze();
            drawingContext.DrawGeometry(null, myPen, streamGeometry);
        }

        private PointCollection GetPolygonPoint(Point center, double r,
            DrawingContext drawingContext = null)
        {
            double g = 18;
            double perangle = 360 / Datas.Count();
            var pi = Math.PI;
            var values = new List<Point>();
            foreach (var item in Datas)
            {
                var p2 = new Point(r * Math.Cos(g * pi / 180) + center.X, r * Math.Sin(g * pi / 180) + center.Y);
                if (drawingContext != null)
                {
                    var formattedText = DrawingContextHelper.GetFormattedText(item.Key, ControlsHelper.PrimaryNormalBrush,
                        flowDirection: FlowDirection.LeftToRight, textSize: 20.001D);
                    if (p2.Y > center.Y && p2.X < center.X)
                        drawingContext.DrawText(formattedText,
                            new Point(p2.X - formattedText.Width - 5, p2.Y - formattedText.Height / 2));
                    else if (p2.Y < center.Y && p2.X > center.X)
                        drawingContext.DrawText(formattedText, new Point(p2.X, p2.Y - formattedText.Height));
                    else if (p2.Y < center.Y && p2.X < center.X)
                        drawingContext.DrawText(formattedText,
                            new Point(p2.X - formattedText.Width - 5, p2.Y - formattedText.Height));
                    else if (p2.Y < center.Y && p2.X == center.X)
                        drawingContext.DrawText(formattedText,
                            new Point(p2.X - formattedText.Width, p2.Y - formattedText.Height));
                    else
                        drawingContext.DrawText(formattedText, new Point(p2.X, p2.Y));
                }

                values.Add(p2);
                g += perangle;
            }

            var pcollect = new PointCollection(values);
            return pcollect;
        }
    }
}

2)示例 ChartRadarExample.xaml 代码如下:

<Border
    Width="700"
    Height="500"
    Background="{DynamicResource WD.BackgroundSolidColorBrush}">
    <Grid Margin="20,10">
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition Width="40" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="40" />
            <RowDefinition />
            <RowDefinition Height="auto" />
        </Grid.RowDefinitions>
        <WrapPanel>
            <Rectangle
                Width="6"
                Height="26"
                Fill="Black" />
            <TextBlock
                Padding="10,0"
                FontSize="24"
                FontWeight="Black"
                Text="能力图" />
            <TextBlock
                VerticalAlignment="Center"
                FontSize="18"
                FontWeight="Black"
                Foreground="#2B579A"
                Text="{Binding NowPlayerName, RelativeSource={RelativeSource AncestorType=local:ChartRadarExample}}" />
        </WrapPanel>
        <wd:ChartRadar
            Grid.Row="1"
            Grid.Column="0"
            Datas="{Binding Datas, RelativeSource={RelativeSource AncestorType=local:ChartRadarExample}}" />
        <Button
            Grid.Row="2"
            Width="200"
            VerticalAlignment="Bottom"
            Click="Button_Click"
            Content="刷新"
            Style="{StaticResource WD.PrimaryButton}" />
    </Grid>
</Border>

3)示例 ChartRadarExample.xaml.cs 代码如下:

  • 定义一个名为 Datas 的公共属性,类型为 IEnumerable<KeyValuePair<string, double>>,用于设置雷达图表中的数据。这个属性使用了依赖属性,可以在 XAML 中进行绑定。

  • NowPlayerName 属性用于显示当前选定的玩家的名称。

  • Players 列表存储了不同的玩家对象,每个玩家都有姓名和各项属性值,如击杀、助攻等。

  • 在构造函数中,初始化了几个玩家对象,并将它们添加到 Players 列表中。然后,通过反射获取了玩家对象的属性信息,并将属性名称和属性值转换为 KeyValuePair<string, double>,并存储在 collectionList 中。

  • 在构造函数末尾,将 Datas 设置为 collectionList 的第一个元素(即第一个玩家的属性数据),并将 NowPlayerName 设置为第一个玩家的姓名。

  • Button_Click 方法用于处理按钮点击事件,切换到下一个玩家的数据,并更新界面上的显示。

public partial class ChartRadarExample : UserControl
 {
     public IEnumerable<KeyValuePair<string, double>> Datas
     {
         get { return (IEnumerable<KeyValuePair<string, double>>)GetValue(DatasProperty); }
         set { SetValue(DatasProperty, value); }
     }

     public static readonly DependencyProperty DatasProperty =
         DependencyProperty.Register("Datas", typeof(IEnumerable<KeyValuePair<string, double>>), typeof(ChartRadarExample), new PropertyMetadata(null));

     List<Player> Players = new List<Player>();
     private int NowPlayerIndex = 0;
     public string NowPlayerName
     {
         get { return (string)GetValue(NowPlayerNameProperty); }
         set { SetValue(NowPlayerNameProperty, value); }
     }
     public static readonly DependencyProperty NowPlayerNameProperty =
  DependencyProperty.Register("NowPlayerName", typeof(string), typeof(ChartRadarExample), new PropertyMetadata(null));

     List<List<KeyValuePair<string, double>>> collectionList = new List<List<KeyValuePair<string, double>>>();
     public ChartRadarExample()
     {
         InitializeComponent();
         Player theShy = new Player()
         {
             姓名 = "The Shy",
             击杀 = 800,
             助攻 = 500,
             物理 = 90,
             生存 = 120,
             金钱 = 360,
             防御 = 230,
             魔法 = 130
         };
         Player xiaoHu = new Player()
         {
             姓名 = "销户",
             击杀 = 50,
             助攻 = 50,
             物理 = 50,
             生存 = 50,
             金钱 = 50,
             防御 = 50,
             魔法 = 50
         };
         Player yinHang = new Player()
         {
             姓名 = "狼行",
             击杀 = 40,
             助攻 = 60,
             物理 = 60,
             生存 = 90,
             金钱 = 40,
             防御 = 80,
             魔法 = 60
         };
         Player flandre = new Player()
         {
             姓名 = "圣枪哥",
             击杀 = 60,
             助攻 = 70,
             物理 = 80,
             生存 = 70,
             金钱 = 80,
             防御 = 100,
             魔法 = 30
         };
         Players.AddRange(new[] { theShy, xiaoHu, yinHang, flandre });

         Type t = theShy.GetType();
         PropertyInfo[] pArray = t.GetProperties();
         pArray = pArray.Where(it => it.PropertyType == typeof(int)).ToArray();

         foreach (var player in Players)
         {
             var collectionpPayer = new List<KeyValuePair<string, double>>();
             Array.ForEach<PropertyInfo>(pArray, p =>
             {
                 collectionpPayer.Add(new KeyValuePair<string, double>( $"{p.Name}({(int)p.GetValue(player, null)}分)", (int)p.GetValue(player, null)));
             });
             collectionList.Add(collectionpPayer);
         }
         Datas = collectionList[0];
         NowPlayerName = Players[0].姓名;
     }

     private void Button_Click(object sender, RoutedEventArgs e)
     {
         NowPlayerIndex++;
         if (NowPlayerIndex >= collectionList.Count)
         {
             NowPlayerIndex = 0;
         }
         Datas = collectionList[NowPlayerIndex];
         NowPlayerName = Players[NowPlayerIndex].姓名;
     }
 }

 public class Player
 {
     public string 姓名 { get; set; }
     public int 击杀 { get; set; }
     public int 生存 { get; set; }
     public int 助攻 { get; set; }
     public int 物理 { get; set; }
     public int 魔法 { get; set; }
     public int 防御 { get; set; }
     public int 金钱 { get; set; }
 }
4f8df860362a4610322a02f50ad6a00d.gif

e7f8e779e20679e377d5aafc9c54bd92.png

参考资料

[1]

原文链接: https://github.com/WPFDevelopersOrg/WPFDevelopers

[2]

码云链接: https://gitee.com/WPFDevelopersOrg/WPFDevelopers

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值