当前位置: 欣欣网 > 码农

WPF 实现折线图

2024-05-15码农

WPF 实现雷达图

控件名:ChartRadar

作 者:WPFDevelopersOrg - 驚鏵

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

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

  • 框架支持 .NET4 至 .NET8

  • Visual Studio 2022 ;

  • 接着

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

    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.BeginFigure geometryContext.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;
    namespaceWPFDevelopers.Controls
    {
    public classChartRadar : ChartBase
    {
    private PointCollection _points;
    privatedouble _h, _w;
    staticChartRadar()
    {
    Default styleKeyProperty.OverrideMetadata(typeof(ChartRadar),
    new FrameworkPropertyMetadata(typeof(ChartRadar)));
    }
    protectedoverridevoidOnRender(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], truetrue);
    geometryContext.PolyLineTo(points, truetrue);
    }
    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);
    }
    }
    privatevoidDrawPoints(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], truetrue);
    geometryContext.PolyLineTo(_points, truetrue);
    }
    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));
    elseif (p2.Y < center.Y && p2.X > center.X)
    drawingContext.DrawText(formattedText, new Point(p2.X, p2.Y - formattedText.Height));
    elseif (p2.Y < center.Y && p2.X < center.X)
    drawingContext.DrawText(formattedText,
    new Point(p2.X - formattedText.Width - 5, p2.Y - formattedText.Height));
    elseif (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}">

    <GridMargin="20,10">
    <Grid.ColumnDefinitions>
    <ColumnDefinition />
    <ColumnDefinitionWidth="40" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
    <RowDefinitionHeight="40" />
    <RowDefinition />
    <RowDefinitionHeight="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 方法用于处理按钮点击事件,切换到下一个玩家的数据,并更新界面上的显示。

  • publicpartial classChartRadarExample : UserControl
     {
    public IEnumerable<KeyValuePair<stringdouble>> Datas
    {
    get { return (IEnumerable<KeyValuePair<stringdouble>>)GetValue(DatasProperty); }
    set { SetValue(DatasProperty, value); }
    }
    publicstaticreadonly DependencyProperty DatasProperty =
    DependencyProperty.Register("Datas"typeof(IEnumerable<KeyValuePair<stringdouble>>), typeof(ChartRadarExample), new PropertyMetadata(null));
    List<Player> Players = new List<Player>();
    privateint NowPlayerIndex = 0;
    publicstring NowPlayerName
    {
    get { return (string)GetValue(NowPlayerNameProperty); }
    set { SetValue(NowPlayerNameProperty, value); }
    }
    publicstaticreadonly DependencyProperty NowPlayerNameProperty =
    DependencyProperty.Register("NowPlayerName"typeof(string), typeof(ChartRadarExample), new PropertyMetadata(null));
    List<List<KeyValuePair<stringdouble>>> collectionList = new List<List<KeyValuePair<stringdouble>>>();
    publicChartRadarExample()
    {
    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<stringdouble>>();
    Array.ForEach<PropertyInfo>(pArray, p =>
    {
    collectionpPayer.Add(new KeyValuePair<stringdouble>( $"{p.Name}{(int)p.GetValue(player, null)}分)", (int)p.GetValue(player, null)));
    });
    collectionList.Add(collectionpPayer);
    }
    Datas = collectionList[0];
    NowPlayerName = Players[0].姓名;
    }
    privatevoidButton_Click(object sender, RoutedEventArgs e)
    {
    NowPlayerIndex++;
    if (NowPlayerIndex >= collectionList.Count)
    {
    NowPlayerIndex = 0;
    }
    Datas = collectionList[NowPlayerIndex];
    NowPlayerName = Players[NowPlayerIndex].姓名;
    }
     }
    public classPlayer
     {
    publicstring 姓名 { getset; }
    publicint 击杀 { getset; }
    publicint 生存 { getset; }
    publicint 助攻 { getset; }
    publicint 物理 { getset; }
    publicint 魔法 { getset; }
    publicint 防御 { getset; }
    publicint 金钱 { getset; }
     }





    参考资料

    [1]

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

    [2]

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