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