WPF-封装自定义雷达图控件
阅读原文时间:2023年08月28日阅读:1

  源码地址:https://gitee.com/LiuShuiRuoBing/code_blog

雷达图用于表示不同内容的占比关系,在项目中有广泛的应用,但是目前未曾有封装良好的雷达图控件,鉴于最近项目的使用,于是想要封装一个通用的雷达图控件,便于日后的扩展使用。

首先雷达图的绘制大概分为雷达图的图层、雷达图的射线、雷达图的有效值区域、雷达图有效值在射线上的点、标题文字等几个部分,封装的初衷就是想要每个部分足够灵活,像图层层数、颜色、射线条数、颜色、粗细文字颜色等属性都开发出来,允许使用者自行设置。这就需要给雷达图控件定义各种不同的依赖属性。

使用自定义控件来实现雷达图的封装,首先创建UserControl,并使用Grid来最为父级容器控件,这里之所以选用Grid而不是Canvas,是因为Canvas上的控件属于绝对定位,而在Grid中更多的是相对定位,查看很多原生空间的样式或者模板会看到更多的使用布局控件作为底层父级容器控件。

关于控件的自定义依赖属性的知识点,在此不再赘述,说几个需要注意的地方,

  • 当要创建可以Binding的集合时,需要定义IEnumerable的类型,注册时PropertyMetadata要使用FrameworkPropertyMetadata

  • 当需要修改属性值,UI会随之更新时,要在注册时,注入PropertyChangedCallback函数

    public IEnumerable<double> LayersPercentList  
    {  
        get { return (IEnumerable<double>)GetValue(LayersPercentListProperty); }  
        set { SetValue(LayersPercentListProperty, value); }  
    }
    
    // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty LayersPercentListProperty =  
        DependencyProperty.Register("LayersPercentList", typeof(IEnumerable<double>), typeof(RadarMapUserControl), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnChangedToRefreshRadarMap)));

雷达图层即雷达图有几层,每一层会有不同的占比,每一层的颜色也不相同,因此分别定义了下面的依赖属性,来对雷达图层进行设置,详细解释请看代码

  • Layers 雷达图层数

  • LayersPercentList 每一层的占比

  • LayerStrokeThickness 每层边框的粗细

  • LayerStroke 每层的边框颜色

  • InnerColor 雷达图从内到外的渐变色,内部颜色

  • OutColor 雷达图从内到外的渐变色,外部颜色

  • Radials 雷达图的射线数

  • Radius 雷达图半径

  • RadialBrush 射线颜色

  • RadialThickness 射线粗细

  • Values ,普通点的集合,可以绑定

  • ValueRadius 普通点的绘制半径

  • ValueBrush 普通点的填充色

  • HeightLightValues, 希望高亮点的集合

  • HeighLightRadius,高亮点的半径

  • HeighLightBrush,高亮点的

  • ValuesAreaFill 值区域的填充色

  • ValuesAreaStroke 值区域的边框色

  • ShowTitle 是否显示标题文字

  • TitleForground 文字的前景色

  • TitleFontSize 文字的字号

  • TitleFontWeight 字重

  • Titles ,集合, 每条射线要现实的文字

RadarMapUserControl.xaml 完整代码

<UserControl x:Class="MT.CustomUserControl.Views.UserControls.RadarMapUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MT.CustomUserControl.Views.UserControls"
mc:Ignorable="d"

         d:Background="White"  
         SizeChanged="RadarMapUserControl\_SizeChanged"  
         >  
<!--雷达图-->  
<Grid x:Name="Grid\_RadarMap" HorizontalAlignment="Center" VerticalAlignment="Center" MinWidth="300" MinHeight="300" Margin="200"  SizeChanged="RadarMapUserControl\_SizeChanged" />

RadarMapUserControl.cs 完整代码

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;

namespace MT.CustomUserControl.Views.UserControls
{
///

/// QSCC_RadarMapUserControl.xaml 的交互逻辑 ///
public partial class RadarMapUserControl : UserControl
{
#region 私有属性,用于绘图

    /// <summary>  
    /// 每个扇区的角度  
    /// </summary>  
    private double Angle { set; get; }

    /// <summary>  
    /// 用于绘制雷达图的层数的多边形  
    /// </summary>  
    private List<Polygon> RadarMapLayersPolygon = new List<Polygon>();

    /// <summary>  
    /// 用于绘制雷达图的射线  
    /// </summary>  
    private List<Polyline> RadarMapRadialsPolyline = new List<Polyline>();

    /// <summary>  
    /// 用于绘制雷达图射线上实际值的圆点,使用多边形绘制,以实际值为圆心扩展多变形  
    /// </summary>  
    private List<Polygon> RadarMapRadialsValuesPolygons = new List<Polygon>();

    /// <summary>  
    /// 所有的雷达图的多变形  
    /// </summary>  
    private Polygon RadarMapRadialsValuesPolygon = new Polygon();

    #endregion

    #region 雷达图图层

    /// <summary>  
    /// 雷达图的层数  
    /// </summary>  
    public int Layers  
    {  
        get { return (int)GetValue(LayersProperty); }  
        set  
        {  
            if (value < 1)  
                value = 1;

            SetValue(LayersProperty, value);  
        }  
    }

    // Using a DependencyProperty as the backing store for Layers.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty LayersProperty =  
        DependencyProperty.Register("Layers", typeof(int), typeof(RadarMapUserControl), new PropertyMetadata(4, new PropertyChangedCallback(OnCurrentLayersChanged)));

    private static void OnCurrentLayersChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)  
    {  
        RadarMapUserControl userControl = (RadarMapUserControl)d;  
        bool needRefresh = false;  
        if (userControl.RadarMapLayersPolygon.Count > userControl.Layers)  
        {  
            int nCnt = userControl.RadarMapLayersPolygon.Count - userControl.Layers;  
            userControl.RadarMapLayersPolygon.RemoveRange(userControl.RadarMapLayersPolygon.Count - 1 - nCnt, nCnt);  
            needRefresh = true;  
        }  
        else if (userControl.RadarMapLayersPolygon.Count < userControl.Layers)  
        {  
            int nCnt = userControl.Layers - userControl.RadarMapLayersPolygon.Count;  
            for (int i = 0; i < nCnt; i++)  
            {  
                userControl.RadarMapLayersPolygon.Add(new Polygon() { Stroke = userControl.LayerStroke, StrokeThickness = 1 });  
            }

            needRefresh = true;  
        }

        if (needRefresh)  
        {  
            userControl.RefreshRadarMap();  
        }  
    }

    /// <summary>  
    /// 雷达图分层的规则,这里使用0-1之间的数据标识,主要是用比例来表示  
    /// 在使用者未指定的情况下,则根据Layers的层数来均分  
    /// 设置举例:雷达图分4层,均分每层面积,则LayersPercentList设置为:  
    /// LayersPercentList\[0\] = 0.25;  
    /// LayersPercentList\[1\] = 0.5;  
    /// LayersPercentList\[2\] = 0.75;  
    /// LayersPercentList\[3\] = 1;  
    /// </summary>  
    public IEnumerable<double> LayersPercentList  
    {  
        get { return (IEnumerable<double>)GetValue(LayersPercentListProperty); }  
        set { SetValue(LayersPercentListProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty LayersPercentListProperty =  
        DependencyProperty.Register("LayersPercentList", typeof(IEnumerable<double>), typeof(RadarMapUserControl), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnChangedToRefreshRadarMap)));

    /// <summary>  
    /// 每层边框的粗细  
    /// </summary>  
    public double LayerStrokeThickness  
    {  
        get { return (double)GetValue(LayerStrokeThicknessProperty); }  
        set { SetValue(LayerStrokeThicknessProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for LayerStrokeThickness.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty LayerStrokeThicknessProperty =  
        DependencyProperty.Register("LayerStrokeThickness", typeof(double), typeof(RadarMapUserControl), new PropertyMetadata(1.0, new PropertyChangedCallback(OnCurrentLayersFillBrushAndStockThicknessChanged)));

    /// <summary>  
    /// 每层的边框颜色  
    /// </summary>  
    public SolidColorBrush LayerStroke  
    {  
        get { return (SolidColorBrush)GetValue(LayerStrokeProperty); }  
        set { SetValue(LayerStrokeProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for LayerStroke.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty LayerStrokeProperty =  
        DependencyProperty.Register("LayerStroke", typeof(SolidColorBrush), typeof(RadarMapUserControl), new PropertyMetadata(Brushes.White, new PropertyChangedCallback(OnCurrentLayersFillBrushAndStockThicknessChanged)));

    /// <summary>  
    /// 雷达图从内到外渐变色,内部颜色  
    /// </summary>  
    public Color InnerColor  
    {  
        get { return (Color)GetValue(InnerColorProperty); }  
        set { SetValue(InnerColorProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for InnerColor.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty InnerColorProperty =  
        DependencyProperty.Register("InnerColor", typeof(Color), typeof(RadarMapUserControl), new PropertyMetadata(Colors.White, new PropertyChangedCallback(OnCurrentLayersFillBrushAndStockThicknessChanged)));

    /// <summary>  
    /// 雷达图从内到外渐变色,外部颜色  
    /// </summary>  
    public Color OutColor  
    {  
        get { return (Color)GetValue(OutColorProperty); }  
        set { SetValue(OutColorProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for OutColor.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty OutColorProperty =  
        DependencyProperty.Register("OutColor", typeof(Color), typeof(RadarMapUserControl), new PropertyMetadata(Colors.Purple, new PropertyChangedCallback(OnCurrentLayersFillBrushAndStockThicknessChanged)));

    private static void OnCurrentLayersFillBrushAndStockThicknessChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)  
    {  
        RadarMapUserControl userControl = (RadarMapUserControl)d;  
        userControl.RefreshLayersFillBrushAndThickness();  
    }

    #endregion

    #region 雷达图射线

    /// <summary>  
    /// 雷达图的射线数  
    /// </summary>  
    public int Radials  
    {  
        get { return (int)GetValue(RadialsProperty); }  
        set { SetValue(RadialsProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for Radials.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty RadialsProperty =  
        DependencyProperty.Register("Radials", typeof(int), typeof(RadarMapUserControl), new PropertyMetadata(9, new PropertyChangedCallback(OnCurrentRadialsChanged)));

    private static void OnCurrentRadialsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)  
    {  
        RadarMapUserControl userControl = (RadarMapUserControl)d;

        bool needRefresh = false;  
        if (userControl.RadarMapRadialsPolyline.Count > userControl.Radials)  
        {  
            int nCnt = userControl.RadarMapRadialsPolyline.Count - userControl.Radials;  
            userControl.RadarMapRadialsPolyline.RemoveRange(userControl.RadarMapRadialsPolyline.Count - 1 - nCnt, nCnt);  
            needRefresh = true;  
        }  
        else if (userControl.RadarMapRadialsPolyline.Count < userControl.Radials)  
        {  
            int nCnt = userControl.Radials - userControl.RadarMapRadialsPolyline.Count;  
            for (int i = 0; i < nCnt; i++)  
            {  
                userControl.RadarMapRadialsPolyline.Add(new Polyline() { Stroke = userControl.RadialBrush, StrokeThickness = 2 });  
            }  
            needRefresh = true;  
        }

        if (userControl.RadarMapRadialsValuesPolygons.Count > userControl.Radials)  
        {  
            int nCnt = userControl.RadarMapRadialsValuesPolygons.Count - userControl.Radials;  
            userControl.RadarMapRadialsValuesPolygons.RemoveRange(userControl.RadarMapRadialsValuesPolygons.Count - 1 - nCnt, nCnt);  
            needRefresh = true;  
        }  
        else if (userControl.RadarMapRadialsValuesPolygons.Count < userControl.Radials)  
        {  
            int nCnt = userControl.Radials - userControl.RadarMapRadialsValuesPolygons.Count;  
            for (int i = 0; i < nCnt; i++)  
            {  
                userControl.RadarMapRadialsValuesPolygons.Add(new Polygon() { Stroke = userControl.LayerStroke, StrokeThickness = 1 });  
            }  
            needRefresh = true;  
        }

        if (needRefresh)  
            userControl.RefreshRadarMap();  
    }

    /// <summary>  
    /// 雷达图半径,决定雷达图的半径  
    /// </summary>  
    public double Radius  
    {  
        get { return (double)GetValue(RadiusProperty); }  
        set { SetValue(RadiusProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for Radius.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty RadiusProperty =  
        DependencyProperty.Register("Radius", typeof(double), typeof(RadarMapUserControl), new PropertyMetadata(100.0, new PropertyChangedCallback(OnChangedToRefreshRadarMap)));

    /// <summary>  
    /// 射线颜色  
    /// </summary>  
    public SolidColorBrush RadialBrush  
    {  
        get { return (SolidColorBrush)GetValue(RadialBrushProperty); }  
        set { SetValue(RadialBrushProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for RadialBrush.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty RadialBrushProperty =  
        DependencyProperty.Register("RadialBrush", typeof(SolidColorBrush), typeof(RadarMapUserControl), new PropertyMetadata(Brushes.White, new PropertyChangedCallback(OnCurrentRadialBrushAndThicknessChanged)));

    /// <summary>  
    /// 射线粗细  
    /// </summary>  
    public double RadialThickness  
    {  
        get { return (double)GetValue(RadialThicknessProperty); }  
        set { SetValue(RadialThicknessProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for RadialThickness.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty RadialThicknessProperty =  
        DependencyProperty.Register("RadialThickness", typeof(double), typeof(RadarMapUserControl), new PropertyMetadata(1.5, new PropertyChangedCallback(OnCurrentRadialBrushAndThicknessChanged)));

    private static void OnCurrentRadialBrushAndThicknessChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)  
    {  
        RadarMapUserControl userControl = (RadarMapUserControl)d;  
        userControl.RefreshRadialBrushAndThinkness();  
    }

    #endregion

    #region 雷达图上点

    /// <summary>  
    /// 射线上的所有值点  
    /// 1. 注意在使用绑定时,要先将Binding对象设置为null,然后将数据整合好的ObservableCollection再赋值给绑定对象,否则不更新  
    /// </summary>  
    public IEnumerable<double> Values  
    {  
        get { return (IEnumerable<double>)GetValue(ValuesProperty); }  
        set { SetValue(ValuesProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for Values.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty ValuesProperty =  
        DependencyProperty.Register("Values", typeof(IEnumerable<double>), typeof(RadarMapUserControl), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnChangedToRefreshValues)));

    private static void OnChangedToRefreshValues(DependencyObject d, DependencyPropertyChangedEventArgs e)  
    {  
        RadarMapUserControl userControl = (RadarMapUserControl)d;  
        userControl.DrawRadarMapRadialsValues();  
    }

    /// <summary>  
    /// 普通值点的绘制半径  
    /// </summary>  
    public double ValueRadius  
    {  
        get { return (double)GetValue(ValueRadiusProperty); }  
        set { SetValue(ValueRadiusProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for ValueRadius.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty ValueRadiusProperty =  
        DependencyProperty.Register("ValueRadius", typeof(double), typeof(RadarMapUserControl), new PropertyMetadata(4.0, new PropertyChangedCallback(OnChangedToRefreshValueRadiusAndBrush)));

    /// <summary>  
    /// 普通值点的颜色  
    /// </summary>  
    public SolidColorBrush ValueBrush  
    {  
        get { return (SolidColorBrush)GetValue(ValueBrushProperty); }  
        set { SetValue(ValueBrushProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for ValueBrush.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty ValueBrushProperty =  
        DependencyProperty.Register("ValueBrush", typeof(SolidColorBrush), typeof(RadarMapUserControl), new PropertyMetadata(Brushes.Red, new PropertyChangedCallback(OnChangedToRefreshValueRadiusAndBrush)));

    /// <summary>  
    /// 需要高亮点的索引  
    /// 1. 注意在使用绑定时,要先将Binding对象设置为null,然后将数据整合好的ObservableCollection再赋值给绑定对象,否则不更新  
    /// </summary>  
    public IEnumerable<double> HeightLightValues  
    {  
        get { return (IEnumerable<double>)GetValue(HeightLightValuesProperty); }  
        set { SetValue(HeightLightValuesProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for HeightLightPoints.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty HeightLightValuesProperty =  
        DependencyProperty.Register("HeightLightValues", typeof(IEnumerable<double>), typeof(RadarMapUserControl), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnChangedToRefreshValues)));

    /// <summary>  
    /// 光亮点的半径  
    /// </summary>  
    public double HeighLightRadius  
    {  
        get { return (double)GetValue(HeighLightRadiusProperty); }  
        set { SetValue(HeighLightRadiusProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for HeighLightValueRadius.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty HeighLightRadiusProperty =  
        DependencyProperty.Register("HeighLightRadius", typeof(double), typeof(RadarMapUserControl), new PropertyMetadata(6.0, new PropertyChangedCallback(OnChangedToRefreshValueRadiusAndBrush)));

    /// <summary>  
    /// 高亮点的颜色  
    /// </summary>  
    public SolidColorBrush HeighLightBrush  
    {  
        get { return (SolidColorBrush)GetValue(HeighLightBrushProperty); }  
        set { SetValue(HeighLightBrushProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty HeighLightBrushProperty =  
        DependencyProperty.Register("HeighLightBrush", typeof(SolidColorBrush), typeof(RadarMapUserControl), new PropertyMetadata(Brushes.Yellow, new PropertyChangedCallback(OnChangedToRefreshValueRadiusAndBrush)));

    private static void OnChangedToRefreshValueRadiusAndBrush(DependencyObject d, DependencyPropertyChangedEventArgs e)  
    {  
        RadarMapUserControl userControl = (RadarMapUserControl)d;  
        userControl.RefreshValuesRadiusAndBrush();  
    }

    #endregion

    #region 雷达图值区域

    /// <summary>  
    /// 雷达图值区域填充色  
    /// </summary>  
    public SolidColorBrush ValuesAreaFill  
    {  
        get { return (SolidColorBrush)GetValue(ValuesAreaFillProperty); }  
        set { SetValue(ValuesAreaFillProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for ValuesAreaFill.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty ValuesAreaFillProperty =  
        DependencyProperty.Register("ValuesAreaFill", typeof(SolidColorBrush), typeof(RadarMapUserControl), new PropertyMetadata(Brushes.Red, new PropertyChangedCallback(OnChangedToRefreshValuesAreaFillAndStrokeBrush)));

    /// <summary>  
    /// 雷达图值区域边框色  
    /// </summary>  
    public SolidColorBrush ValuesAreaStroke  
    {  
        get { return (SolidColorBrush)GetValue(ValuesAreaStrokeProperty); }  
        set { SetValue(ValuesAreaStrokeProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for ValuesAreaStroke.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty ValuesAreaStrokeProperty =  
        DependencyProperty.Register("ValuesAreaStroke", typeof(SolidColorBrush), typeof(RadarMapUserControl), new PropertyMetadata(Brushes.Gray, new PropertyChangedCallback(OnChangedToRefreshValuesAreaFillAndStrokeBrush)));

    private static void OnChangedToRefreshValuesAreaFillAndStrokeBrush(DependencyObject d, DependencyPropertyChangedEventArgs e)  
    {  
        RadarMapUserControl userControl = (RadarMapUserControl)d;  
        userControl.RefreshValuesAreaBrushAndStroke();  
    }

    #endregion

    #region 雷达图的射线标题文字

    /// <summary>  
    /// 是否显示Title  
    /// </summary>  
    public bool ShowTitle  
    {  
        get { return (bool)GetValue(ShowTitleProperty); }  
        set { SetValue(ShowTitleProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for ShowTitle.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty ShowTitleProperty =  
        DependencyProperty.Register("ShowTitle", typeof(bool), typeof(RadarMapUserControl), new PropertyMetadata(false, new PropertyChangedCallback(OnChangedToRefreshTitles)));

    /// <summary>  
    /// 文字的前景色  
    /// </summary>  
    public SolidColorBrush TitleForground  
    {  
        get { return (SolidColorBrush)GetValue(TitleForgroundProperty); }  
        set { SetValue(TitleForgroundProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for TitleForground.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty TitleForgroundProperty =  
        DependencyProperty.Register("TitleForground", typeof(SolidColorBrush), typeof(RadarMapUserControl), new PropertyMetadata(Brushes.Black, new PropertyChangedCallback(OnChangedToRefreshTitles)));

    /// <summary>  
    /// 文字的字号  
    /// </summary>  
    public int TitleFontSize  
    {  
        get { return (int)GetValue(TitleFontSizeProperty); }  
        set { SetValue(TitleFontSizeProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for TitleFontSize.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty TitleFontSizeProperty =  
        DependencyProperty.Register("TitleFontSize", typeof(int), typeof(RadarMapUserControl), new PropertyMetadata(14, new PropertyChangedCallback(OnChangedToRefreshTitles)));

    /// <summary>  
    /// FontWeight  
    /// </summary>  
    public FontWeight TitleFontWeight  
    {  
        get { return (FontWeight)GetValue(TitleFontWeightProperty); }  
        set { SetValue(TitleFontWeightProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for TitleFontWeights.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty TitleFontWeightProperty =  
        DependencyProperty.Register("TitleFontWeights", typeof(FontWeight), typeof(RadarMapUserControl), new PropertyMetadata(FontWeights.Normal, new PropertyChangedCallback(OnChangedToRefreshTitles)));

    /// <summary>  
    /// Title要显示的文字  
    /// </summary>  
    public IEnumerable<string> Titles  
    {  
        get { return (IEnumerable<string>)GetValue(TitlesProperty); }  
        set { SetValue(TitlesProperty, value); }  
    }

    // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty TitlesProperty =  
        DependencyProperty.Register("Titles", typeof(IEnumerable<string>), typeof(RadarMapUserControl), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnChangedToRefreshTitles)));

    private static void OnChangedToRefreshTitles(DependencyObject d, DependencyPropertyChangedEventArgs e)  
    {  
        RadarMapUserControl userControl = (RadarMapUserControl)d;  
        userControl.RefreshRadarMap();  
    }

    #endregion

    private static void OnChangedToRefreshRadarMap(DependencyObject d, DependencyPropertyChangedEventArgs e)  
    {  
        RadarMapUserControl userControl = (RadarMapUserControl)d;  
        userControl.RefreshRadarMap();  
    }

    public RadarMapUserControl()  
    {  
        SolidColorBrush polygonFill = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#7653a7"));

        //绘制图层  
        List<Color> colors = GetSingleColorList(OutColor, InnerColor, Layers);  
        for (int i = 0; i < Layers; i++)  
        {  
            RadarMapLayersPolygon.Add(new Polygon() {Fill= new SolidColorBrush(colors\[i\]),  Stroke = LayerStroke, StrokeThickness = LayerStrokeThickness });  
        }

        //绘制射线以及线上值  
        for (int i = 0; i < Radials; i++)  
        {  
            RadarMapRadialsPolyline.Add(new Polyline() { Stroke = RadialBrush, StrokeThickness = RadialThickness });  
            RadarMapRadialsValuesPolygons.Add(new Polygon() { Fill = ValueBrush, StrokeThickness = 1 });  
        }

        //雷达图值组成的区域  
        RadarMapRadialsValuesPolygon = new Polygon() { Fill = ValuesAreaFill, Stroke = ValuesAreaStroke, Opacity = 0.2 };

        InitializeComponent();  
    }

    private void RadarMapUserControl\_SizeChanged(object sender, SizeChangedEventArgs e)  
    {  
        RefreshRadarMap();  
    }

    /// <summary>  
    /// 刷新雷达图的层的填充色和层线粗细  
    /// </summary>  
    private void RefreshLayersFillBrushAndThickness()  
    {  
        //绘制雷达图层的多边形  
        List<Color> colors = GetSingleColorList(OutColor, InnerColor, Layers);  
        for (int i = 0; i < Layers; i++)  
        {  
            RadarMapLayersPolygon\[i\].Fill = new SolidColorBrush(colors\[i\]);  
            RadarMapLayersPolygon\[i\].Stroke = LayerStroke;  
            RadarMapLayersPolygon\[i\].StrokeThickness = LayerStrokeThickness;  
        }  
    }

    /// <summary>  
    /// 刷新射线的颜色和粗细  
    /// </summary>  
    private void RefreshRadialBrushAndThinkness()  
    {  
        foreach (var item in RadarMapRadialsPolyline)  
        {  
            item.Stroke = RadialBrush;  
            item.StrokeThickness = RadialThickness;  
        }  
    }

    /// <summary>  
    /// 刷新雷达图  
    /// </summary>  
    private void RefreshRadarMap()  
    {  
        Grid\_RadarMap.Children.Clear();

        //首先清除一下polygon里存储的数据  
        for (int i = 0; i < Layers; i++)  
        {  
            RadarMapLayersPolygon\[i\]?.Points?.Clear();  
        }

        for (int i = 0; i < Radials; i++)  
        {  
            RadarMapRadialsPolyline\[i\]?.Points?.Clear();  
        }

        //如果设置了LayersPercentList,并且LayersPercentList的元素个数与层数相同则按照LayersPercentList画每层的占比,否则均分每层占比  
        List<double> layersPercents = new List<double>();  
        if (LayersPercentList != null && LayersPercentList.Count() == Layers && LayersPercentList.Max() < 1)  
        {  
            foreach (var item in LayersPercentList)  
            {  
                layersPercents.Add(item);  
            }  
        }  
        else  
        {  
            double gap = 1.0 / Layers;  
            for (int i = 0; i < Layers; i++)  
            {  
                layersPercents.Add(gap \* i + gap); //计算每层的默认占比  
            }  
        }

        //计算每个扇区的角度  
        Angle = 360 / Radials;

        //计算并添加雷达图的区域线和射线上的点  
        for (int i = 0; i < Radials; i++)  
        {  
            //射线上每层的点,从外到内  
            List<Point> points = new List<Point>();  
            for (int j = 0; j < Layers; j++)  
            {  
                Point p = new Point(Radius + (Radius \* layersPercents\[Layers - j - 1\]) \* Math.Cos((Angle \* i - 90) \* Math.PI / 180),  
                                    Radius + (Radius \* layersPercents\[Layers - j - 1\]) \* Math.Sin((Angle \* i - 90) \* Math.PI / 180));

                points.Add(p);

                //添加到区域线中  
                RadarMapLayersPolygon\[j\].Points.Add(p);  
            }

            //添加到射线中  
            foreach (var item in points)  
            {  
                RadarMapRadialsPolyline\[i\].Points.Add(item);  
            }

            //计算原点并添加到射线中  
            Point p\_origin = new Point(Radius + Radius \* 0 \* Math.Cos((Angle \* i - 90) \* Math.PI / 180),  
                                       Radius + Radius \* 0 \* Math.Sin((Angle \* i - 90) \* Math.PI / 180));

            RadarMapRadialsPolyline\[i\].Points.Add(p\_origin);  
        }

        //绘制区域层  
        foreach (var polygon in RadarMapLayersPolygon)  
        {  
            if (!Grid\_RadarMap.Children.Contains(polygon))  
                Grid\_RadarMap.Children.Add(polygon);  
        }

        //绘制雷达图射线  
        foreach (var polyline in RadarMapRadialsPolyline)  
        {  
            if (!Grid\_RadarMap.Children.Contains(polyline))  
                Grid\_RadarMap.Children.Add(polyline);  
        }

        //绘制雷达图上的文字  
        if (ShowTitle && Titles != null && Titles.Count() == Radials)  
        {  
            List<string> titleList = Titles.ToList();  
            for (int i = 0; i < Radials; i++)  
            {  
                Point point = RadarMapLayersPolygon\[0\].Points\[i\];  
                string title = titleList\[i\];  
                TextBlock textBlock = RefreshRadiusTitles(point, title);

                if (!Grid\_RadarMap.Children.Contains(textBlock))  
                    Grid\_RadarMap.Children.Add(textBlock);  
            }  
        }

        DrawRadarMapRadialsValues();  
    }

    /// <summary>  
    /// 刷新雷达图上值的点的半径和填充色,以及高亮点的半径和填充色  
    /// </summary>  
    private void RefreshValuesRadiusAndBrush()  
    {  
        if (Values == null)  
            return;

        bool drawHeight = false;  
        if (HeightLightValues != null && HeightLightValues.Count() > 0)  
            drawHeight = true;

        List<double> values = Values.ToList();  
        for (int i = 0; i < RadarMapRadialsValuesPolygon.Points.Count; i++)  
        {  
            RadarMapRadialsValuesPolygons\[i\].Points.Clear();  
            RadarMapRadialsValuesPolygons\[i\].Fill = ValueBrush;

            double radius = ValueRadius;

            if (drawHeight)  
            {  
                if (HeightLightValues.Contains(values\[i\]))  
                {  
                    radius = HeighLightRadius;  
                    RadarMapRadialsValuesPolygons\[i\].Fill = HeighLightBrush;

                    if (ShowTitle && Titles != null && Titles.Count() > i)  
                    {  
                        List<string> titleList = Titles.ToList();  
                        string heightTitle = titleList\[i\];  
                        foreach (var item in Grid\_RadarMap.Children)  
                        {  
                            if (item is TextBlock)  
                            {  
                                TextBlock textBlock = (TextBlock)item;  
                                if (textBlock.Text == heightTitle)  
                                {  
                                    textBlock.Foreground = HeighLightBrush;  
                                }  
                            }  
                        }  
                    }  
                }  
            }

            Point valuePoint = RadarMapRadialsValuesPolygon.Points\[i\];  
            Point\[\] calc\_points = GetEllipsePoints(valuePoint, radius);

            foreach (var p in calc\_points)  
            {  
                RadarMapRadialsValuesPolygons\[i\].Points.Add(p);  
            }

            if (!Grid\_RadarMap.Children.Contains(RadarMapRadialsValuesPolygons\[i\]))  
                Grid\_RadarMap.Children.Add(RadarMapRadialsValuesPolygons\[i\]);  
        }  
    }

    /// <summary>  
    /// 刷新雷达图值区域的填充色和边框色  
    /// </summary>  
    private void RefreshValuesAreaBrushAndStroke()  
    {  
        RadarMapRadialsValuesPolygon.Fill = ValuesAreaFill;  
        RadarMapRadialsValuesPolygon.Stroke = ValuesAreaStroke;  
    }

    /// <summary>  
    /// 刷新射线上的文字标题  
    /// </summary>  
    /// <param name="point">图层最外层的点</param>  
    /// <returns></returns>  
    private TextBlock RefreshRadiusTitles(Point point, string title)  
    {  
        TextBlock textBlock = new TextBlock();

        textBlock.FontSize = 20;  
        textBlock.Text = title;  
        textBlock.Foreground = TitleForground;  
        textBlock.FontWeight = FontWeights.Normal;  
        textBlock.FontSize = TitleFontSize;

        //计算文字的实际像素值  
        Rect rect1 = new Rect();  
        textBlock.Arrange(rect1);  
        double textLength = textBlock.ActualWidth;

        Thickness thickness = new Thickness(point.X + 10, point.Y - 10, 0, 0);  
        if (point.X == Radius && point.Y < Radius)  
        {  
            thickness = new Thickness(point.X - textLength / 2, point.Y - 30, 0, 0);  
        }  
        else if (point.X == Radius && point.Y >= Radius)  
        {  
            thickness = new Thickness(point.X - textLength / 2, point.Y + 10, 0, 0);  
        }  
        else if (point.X < Radius)  
        {  
            thickness = new Thickness(point.X - 20 - textLength, point.Y - 10, 0, 0);  
        }  
        else  
        {  
            thickness = new Thickness(point.X + 10, point.Y - 10, 0, 0);  
        }

        textBlock.Margin = thickness;

        return textBlock;  
    }

    /// <summary>  
    /// 绘制雷达图上的点  
    /// </summary>  
    /// <param name="Values"></param>  
    /// <param name="mainType"></param>  
    /// <param name="secondType"></param>  
    public void DrawRadarMapRadialsValues()  
    {  
        if (Values == null || Values.Count() != Radials)  
            return;

        int fullScore = 100;

        RadarMapRadialsValuesPolygon.Points.Clear();  
        for (int i = 0; i < Radials; i++)  
        {  
            double temp = Values.ToList()\[i\];

            if (temp <= 0)  
                continue;

            Point value = new Point(Radius + Radius \* (temp \* 1.0 / fullScore) \* Math.Cos((Angle \* i - 90) \* Math.PI / 180),  
                                    Radius + Radius \* (temp \* 1.0 / fullScore) \* Math.Sin((Angle \* i - 90) \* Math.PI / 180));

            RadarMapRadialsValuesPolygon.Points.Add(value);  
        }

        if (!Grid\_RadarMap.Children.Contains(RadarMapRadialsValuesPolygon))  
            Grid\_RadarMap.Children.Add(RadarMapRadialsValuesPolygon);

        RefreshValuesRadiusAndBrush();  
    }

    #region 工具类

    /// <summary>  
    /// 根据圆心,扩展绘制圆  
    /// </summary>  
    /// <param name="origin"></param>  
    /// <param name="radius"></param>  
    /// <returns></returns>  
    private Point\[\] GetEllipsePoints(Point origin, double radius)  
    {  
        int count = 10;  
        Point\[\] points = new Point\[count\];

        double angle = 360 / count;

        for (int i = 0; i < count; i++)  
        {  
            Point p1 = new Point(origin.X + radius \* Math.Cos((angle \* i - 90) \* Math.PI / 180),  
                                 origin.Y + radius \* Math.Sin((angle \* i - 90) \* Math.PI / 180));

            points\[i\] = p1;  
        }

        return points;  
    }

    /// <summary>  
    /// 获得某一颜色区间的颜色集合  
    /// </summary>  
    /// <param name="sourceColor">起始颜色</param>  
    /// <param name="destColor">终止颜色</param>  
    /// <param name="count">分度数</param>  
    /// <returns>返回颜色集合</returns>  
    private List<Color> GetSingleColorList(Color srcColor, Color desColor, int count)  
    {  
        List<Color> colorFactorList = new List<Color>();  
        int redSpan = desColor.R - srcColor.R;  
        int greenSpan = desColor.G - srcColor.G;  
        int blueSpan = desColor.B - srcColor.B;  
        for (int i = 0; i < count; i++)  
        {  
            Color color = Color.FromRgb(  
                (byte)(srcColor.R + (int)((double)i / count \* redSpan)),  
                (byte)(srcColor.G + (int)((double)i / count \* greenSpan)),  
                (byte)(srcColor.B + (int)((double)i / count \* blueSpan))  
            );  
            colorFactorList.Add(color);  
        }  
        return colorFactorList;  
    }

    #endregion  
}  

}