當前位置: 妍妍網 > 碼農

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