WPF 玫瑰图绘制

1.前言:

一直在从事CS应用程序开发工作,随着工作需求,要对部分数据进行可视化展示,UI设计稿其中就有玫瑰图、雷达图的展示。

花了一个下午回溯原来丢掉的数学知识点。。特此将实现方法记录下。

2.效果图:

WPF 玫瑰图绘制

 

 

 

 3.数据对象(RadarObj)

  每个图都是由一个数据集合对象组成,从而绘制出对应的效果,对象最基本的属性要有某一维度的数值,用于在图像中展示。

 

    public class RadarObj
    {
        public string RColor { get; set; }
        public string Name { get; set; }
        public int DataValue { get; set; }
        public double DataRaidus { get; set; }

        /// <summary>
        /// Series stroke
        /// </summary>
        public Brush Stroke
        {
            get
            {
                return new SolidColorBrush((Color)ColorConverter.ConvertFromString(RColor)); ;
            }
        }

        /// <summary>
        /// Series Fill
        /// </summary>
        public Brush Fill
        {
            get
            {
                return new SolidColorBrush((Color)ColorConverter.ConvertFromString(RColor));
            }
        }
    }

  

 4.绘制玫瑰图

 核心逻辑在于,将玫瑰图先理解为一个饼图,然后根据数值计算出在饼图中占用的角度,以及对应的扇面半径,改动每个扇面的半径就成了玫瑰图

 其中需要使用到几何的一些基本概念,工作这么多年,忘记了蛮多的,后面各种恶补。直接上代码吧。

<UserControl x:Class="Painter.NightingaleRose"
             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:Painter"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Canvas x:Name="CanvasPanel" HorizontalAlignment="Center" VerticalAlignment="Center" Background="Gray" >
        </Canvas>
    </Grid>
</UserControl>
 public partial class NightingaleRose : UserControl
    {
        public NightingaleRose()
        {
            InitializeComponent();
        }

        #region Property

        /// <summary>
        /// 数据
        /// </summary>
        public List<RadarObj> Datas
        {
            get { return (List<RadarObj>)GetValue(DatasProperty); }
            set { SetValue(DatasProperty, value); }
        }

        /// <summary>
        /// 数值的总数
        /// </summary>
        public int Count
        {
            get
            {
                return Datas.Sum(i => i.DataValue);
            }
        }

        public static readonly DependencyProperty DatasProperty = DependencyProperty.Register("Datas", typeof(List<RadarObj>),
        typeof(NightingaleRose), new PropertyMetadata(new List<RadarObj>()));

        /// <summary>
        /// 当前绘制大区域
        /// </summary>
        private double MaxSize
        {
            get
            {
                var par = this.Parent as FrameworkElement;
                return par.ActualHeight > par.ActualWidth ? par.ActualWidth : par.ActualHeight;
            }
        }

        /// <summary>
        /// 停靠间距
        /// </summary>
        public int RoseMargin
        {
            get { return (int)GetValue(RoseMarginProperty); }
            set { SetValue(RoseMarginProperty, value); }
        }

        public static readonly DependencyProperty RoseMarginProperty = DependencyProperty.Register("RoseMargin", typeof(int),
        typeof(NightingaleRose), new PropertyMetadata(50));

        /// <summary>
        /// 空心内环半径
        /// </summary>
        public int RoseInsideMargin
        {
            get { return (int)GetValue(RoseInsideMarginProperty); }
            set { SetValue(RoseInsideMarginProperty, value); }
        }

        public static readonly DependencyProperty RoseInsideMarginProperty = DependencyProperty.Register("RoseInsideMargin", typeof(int),
        typeof(NightingaleRose), new PropertyMetadata(20));

        /// <summary>
        /// 显示值标注
        /// </summary>
        public bool ShowValuesLabel
        {
            get { return (bool)GetValue(ShowValuesLabelProperty); }
            set { SetValue(ShowValuesLabelProperty, value); }
        }

        public static readonly DependencyProperty ShowValuesLabelProperty = DependencyProperty.Register("ShowValuesLabel", typeof(bool),
        typeof(NightingaleRose), new PropertyMetadata(true));


        public static readonly DependencyProperty ShowToolTipProperty = DependencyProperty.Register("ShowToolTip", typeof(bool),
        typeof(NightingaleRose), new PropertyMetadata(false));

        /// <summary>
        /// 延伸线长
        /// </summary>
        public int LabelPathLength
        {
            get { return (int)GetValue(LabelPathLengthProperty); }
            set { SetValue(LabelPathLengthProperty, value); }
        }

        public static readonly DependencyProperty LabelPathLengthProperty = DependencyProperty.Register("LabelPathLength", typeof(int),
        typeof(NightingaleRose), new PropertyMetadata(50));

        #endregion Property

        #region Method

        /// <summary>
        /// 初始化数据
        /// </summary>
        private void initData()
        {
            CanvasPanel.Children.Clear();
            if (this.Datas != null && this.Datas.Count > 0)
            {
                this.CanvasPanel.Width = this.CanvasPanel.Height = 0;

                //求角度比例尺  (每个值占多大的角度  可以算到每一块图所占的角度)
                var angelScale = 360.00 / Datas.Sum(i => i.DataValue);

                //最大半径
                var maxRadius = (MaxSize / 2) - RoseMargin - (ShowValuesLabel ? LabelPathLength : 0);

                //半径比例尺  (值和比例尺相乘等于每一块图的半径)
                var radiusScale = maxRadius / Datas.Max(o => o.DataValue);

                //计算半径宽度值
                for (int i = 0; i < Datas.Count; i++)
                {
                    Datas[i].DataRaidus = Datas[i].DataValue * radiusScale;
                }
                //扇形角度初始化
                double angleSectorStart = 0;
                double angleSectorEnd = 0;

                //循环绘制扇形区域
                int scaleTimeSpan = 0;
                int pathTimespan = 0;
                int textTimeSpan = 0;
                for (int index = 0; index < Datas.Count; index++)
                {
                    //计算扇形角度
                    if (index == 0)
                    {
                        angleSectorStart = 0;
                        angleSectorEnd = Datas[index].DataValue * angelScale;
                    }
                    else if (index + 1 == Datas.Count)
                    {
                        angleSectorStart += Datas[index - 1].DataValue * angelScale;
                        angleSectorEnd = 360;
                    }
                    else
                    {
                        angleSectorStart += Datas[index - 1].DataValue * angelScale;
                        angleSectorEnd = angleSectorStart + Datas[index].DataValue * angelScale;
                    }
                    var currentRadius = RoseInsideMargin + Datas[index].DataRaidus;
                    //计算扇形点位,用于绘制PATH
                    Point ptOutSideStart = GetPoint(currentRadius, angleSectorStart * Math.PI / 180);
                    Point ptOutSideEnd = GetPoint(currentRadius, angleSectorEnd * Math.PI / 180);
                    Point ptInSideStart = GetPoint(RoseInsideMargin, angleSectorStart * Math.PI / 180);
                    Point ptInSideEnd = GetPoint(RoseInsideMargin, angleSectorEnd * Math.PI / 180);
                    if (string.IsNullOrEmpty(Datas[index].RColor) )
                        Datas[index].RColor = ChartColorPool.ColorStrings[index];
                    Path pthSector = new Path() { Fill = Datas[index].Fill };
                    //PATH数据格式 M0,100  L50,100 A50,50 0 0 1 100,50 L100,0 A100,100 0 0 0 0,100 Z
                    StringBuilder datastrb = new StringBuilder();

                    #region BuilderPathData

                    datastrb.Append("M");
                    datastrb.Append(ptOutSideStart.X.ToString());
                    datastrb.Append(",");
                    datastrb.Append(ptOutSideStart.Y.ToString());
                    datastrb.Append(" L");
                    datastrb.Append(ptInSideStart.X.ToString());
                    datastrb.Append(",");
                    datastrb.Append(ptInSideStart.Y.ToString());
                    datastrb.Append(" A");
                    datastrb.Append(RoseInsideMargin.ToString());
                    datastrb.Append(",");
                    datastrb.Append(RoseInsideMargin.ToString());
                    datastrb.Append(" 0 0 1 ");
                    datastrb.Append(ptInSideEnd.X.ToString());
                    datastrb.Append(",");
                    datastrb.Append(ptInSideEnd.Y.ToString());
                    datastrb.Append(" L");
                    datastrb.Append(ptOutSideEnd.X.ToString());
                    datastrb.Append(",");
                    datastrb.Append(ptOutSideEnd.Y.ToString());
                    datastrb.Append(" A");
                    datastrb.Append(currentRadius.ToString());
                    datastrb.Append(",");
                    datastrb.Append(currentRadius.ToString());
                    datastrb.Append(" 0 0 0 ");
                    datastrb.Append(ptOutSideStart.X.ToString());
                    datastrb.Append(",");
                    datastrb.Append(ptOutSideStart.Y.ToString());
                    datastrb.Append(" Z");

                    #endregion BuilderPathData
                    try
                    {
                        pthSector.Data = (Geometry)new GeometryConverter().ConvertFromString(datastrb.ToString());
                    }
                    catch (Exception exp)
                    { }
                    //设置扇形显示的动画
                    AnimationUtils.FloatElement(pthSector,1, 200, pathTimespan += 200);
                    AnimationUtils.ScaleRotateEasingAnimationShow(pthSector,0.1,1,1500, scaleTimeSpan += 200,null );

                    CanvasPanel.Children.Add(pthSector);
                    if (ShowValuesLabel)
                    {
                        //计算延伸线角度
                        double lbPathAngle = angleSectorStart + (angleSectorEnd - angleSectorStart) / 2;
                        //起点
                        Point ptLbStart = GetPoint(currentRadius, lbPathAngle * Math.PI / 180);
                        //终点
                        Point ptLbEnd = GetPoint(maxRadius + LabelPathLength, lbPathAngle * Math.PI / 180);
                        Path pthLb = new Path() { Stroke = Datas[index].Stroke, StrokeThickness = 1 };
                        pthLb.Data = (Geometry)new GeometryConverter().ConvertFromString(string.Format("M{0},{1} {2},{3}", ptLbStart.X.ToString(), ptLbStart.Y.ToString(), ptLbEnd.X.ToString(), ptLbEnd.Y.ToString()));
                        double dur = (textTimeSpan += 200) + 1500;
                        AnimationUtils.CtrlDoubleAnimation(pthLb, 1000, dur);
                        CanvasPanel.Children.Add(pthLb);
                        SetLabel(Datas[index], ptLbEnd, dur);
                    }
                }
                this.SizeChanged -= RadarControl_SizeChanged;
                this.SizeChanged += RadarControl_SizeChanged;
            }
        }

        public void InitalControl()
        {
        }

        /// <summary>
        /// 初始化数据
        /// </summary>
        /// <param name="dataobj"></param>
        public void SetData(object dataobj)
        {
            this.Datas = (dataobj) as List<RadarObj>;
            this.initData();
        }

        private void RadarControl_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            initData();
        }

        #endregion Method

        #region Compare

        /// <summary>
        /// 计算点位
        /// </summary>
        /// <param name="radius"></param>
        /// <param name="angel"></param>
        /// <returns></returns>
        private Point GetPoint(double radius, double angel)
        {
            return new Point(radius * Math.Cos(angel), radius * Math.Sin(angel));
        }

        private void SetLabel(RadarObj obj, Point location,double duration)
        {
            //计算偏移量
            bool x = true;
            bool y = true;

            if (location.X < 0)
                x = false;
            if (location.Y < 0)
                y = false;
            //obj.Name + " " + 
            TextBlock txb = new TextBlock() { Text = " "+obj.Name+" "+Getbfb(Count.ToString(), obj.DataValue.ToString(), 2)+ " ", Foreground = this.Foreground, FontSize = this.FontSize };
            Size s = ControlSizeUtils.GetTextAreaSize(txb.Text, this.FontSize);
            CanvasPanel.Children.Add(txb);
            AnimationUtils.CtrlDoubleAnimation(txb, 1000, duration);
            if (location.X > -5 && location.X < 5)
                Canvas.SetLeft(txb, location.X - (s.Width / 2));
            else
                Canvas.SetLeft(txb, location.X + (x ? 0 : -(s.Width)));

            if (location.Y > -5 && location.Y < 5)
                Canvas.SetTop(txb, location.Y - (s.Height / 2));
            else
                Canvas.SetTop(txb, location.Y + (y ? 0 : -(s.Height)));
        }
        /// <summary>
        /// 计算百分比
        /// </summary>
        /// <param name="zs">总数</param>
        /// <param name="tj">当前项的值</param>
        /// <param name="num">保留的小数点几位</param>
        /// <returns></returns>
        public static string Getbfb(string zs, string tj, int num)
        {
            try
            {
                if (zs.Equals("0"))
                {
                    return "0";
                }
                double bfb = (double.Parse(tj) / double.Parse(zs)) * 100;
                if (bfb >= 100)
                {
                    bfb = 100;
                }

                return Math.Round(bfb, num).ToString() + "%";
            }
            catch (Exception ex)
            {
                return "0%";
            }
        }

        #endregion Compare

       
    }

 

调用示例

 在主窗体grid里面丢个按钮,按钮点击之后执行下列代码

 

private void RoseClick(object sender, RoutedEventArgs e)
        {
            NightingaleRose rdc = new NightingaleRose();
            this.GrdMain.Children.Clear();
            this.GrdMain.Children.Add(rdc);
            rdc.SetData(CrData());
        }
        private List<RadarObj> CrData()
        {
            List<RadarObj> list = new List<RadarObj>();
            list.Add(new RadarObj() { Name="A", DataValue= rdm.Next(20,100) });
            list.Add(new RadarObj() { Name = "B", DataValue = rdm.Next(20, 100) });
            list.Add(new RadarObj() { Name = "C", DataValue = rdm.Next(20, 100) });
            list.Add(new RadarObj() { Name = "D", DataValue = rdm.Next(20, 100) });
            list.Add(new RadarObj() { Name = "E", DataValue = rdm.Next(20, 100) });
            list.Add(new RadarObj() { Name = "F", DataValue = rdm.Next(20, 100) });
            list.Add(new RadarObj() { Name = "F", DataValue = rdm.Next(20, 100) });
            return list;
        }

  

WPF 玫瑰图绘制

上一篇:WWDC 2014 Session笔记 - iOS界面开发的大一统


下一篇:C# 杀掉系统中的进程