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], 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);
}
}
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], 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));
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<string, double>> Datas
{
get { return (IEnumerable<KeyValuePair<string, double>>)GetValue(DatasProperty); }
set { SetValue(DatasProperty, value); }
}
publicstaticreadonly DependencyProperty DatasProperty =
DependencyProperty.Register("Datas", typeof(IEnumerable<KeyValuePair<string, double>>), 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<string, double>>> collectionList = new List<List<KeyValuePair<string, double>>>();
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<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].姓名;
}
privatevoidButton_Click(object sender, RoutedEventArgs e)
{
NowPlayerIndex++;
if (NowPlayerIndex >= collectionList.Count)
{
NowPlayerIndex = 0;
}
Datas = collectionList[NowPlayerIndex];
NowPlayerName = Players[NowPlayerIndex].姓名;
}
}
public classPlayer
{
publicstring 姓名 { get; set; }
publicint 击杀 { get; set; }
publicint 生存 { get; set; }
publicint 助攻 { get; set; }
publicint 物理 { get; set; }
publicint 魔法 { get; set; }
publicint 防御 { get; set; }
publicint 金钱 { get; set; }
}
参考资料
[1]
原文链接:
https://github.com/WPFDevelopersOrg/WPFDevelopers
码云链接:
https://gitee.com/WPFDevelopersOrg/WPFDevelopers