WPF 实现Circle控件(原Arc控件和Pie控件的合并版)

前情导航:WPF 自实现Arc控件和Pie控件

看过上一篇文章的应该知道,Arc和Pie的实现方式区别不过是一个把Path路径的终点与起点相连,一个没有相连,于是本人就索性把二者合二为一了。决定是画Arc还是画Pie,完全根据Stroke和Fill两个属性来判断。

废话不多说,终极代码奉上:

public class Circle : Shape
{
    #region 成员变量
    private Geometry m_Data;
    private Geometry Data
    {
        get { return m_Data; }
        set
        {
            if (m_Data != value)
            {
                m_Data = value;
                this.InvalidateVisual();
            }
        }
    }

    private CircleStatus m_Status;
    #endregion

    #region 重写方法
    /// <summary>
    /// 监听StrokeThickness变化
    /// </summary>
    /// <param name="e"></param>
    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        base.OnPropertyChanged(e);
        if (e.Property == StrokeThicknessProperty)
        {
            propertyChangedCallback(this, new DependencyPropertyChangedEventArgs());
        }
        else if (e.Property == FillProperty)
        {
            m_Status = e.NewValue == null ? CircleStatus.Ring : CircleStatus.Pie;
            propertyChangedCallback(this, new DependencyPropertyChangedEventArgs());
        }
        else if (e.Property == WidthProperty || e.Property == HeightProperty || e.Property == ActualWidthProperty || e.Property == ActualHeightProperty)
        {
            propertyChangedCallback(this, new DependencyPropertyChangedEventArgs());
        }
    }

    /// <summary>
    /// 重写此方法实现图形绘制
    /// </summary>
    protected override Geometry DefiningGeometry { get { return Data; } }
    #endregion

    #region 依赖属性
    /// <summary>
    /// 起始角度
    /// </summary>
    public double StartAngle
    {
        get { return (double)GetValue(StartAngleProperty); }
        set { SetValue(StartAngleProperty, value); }
    }

    // Using a DependencyProperty as the backing store for StartAngle.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty StartAngleProperty =
        DependencyProperty.Register("StartAngle", typeof(double), typeof(Circle), new PropertyMetadata(0.0, propertyChangedCallback));

    /// <summary>
    /// 终止角度
    /// </summary>
    public double EndAngle
    {
        get { return (double)GetValue(EndAngleProperty); }
        set { SetValue(EndAngleProperty, value); }
    }

    // Using a DependencyProperty as the backing store for EndAngle.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty EndAngleProperty =
        DependencyProperty.Register("EndAngle", typeof(double), typeof(Circle), new PropertyMetadata(0.0, propertyChangedCallback));

    /// <summary>
    /// 内边距
    /// </summary>
    public double Padding
    {
        get { return (double)GetValue(PaddingProperty); }
        set { SetValue(PaddingProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Padding.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty PaddingProperty =
        DependencyProperty.Register("Padding", typeof(double), typeof(Circle), new PropertyMetadata(0.0, propertyChangedCallback));

    #endregion

    #region 图形语句生成
    private static void propertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is Circle circle)
        {
            // 计算内边距
            var padding = circle.m_Status == CircleStatus.Ring ? circle.StrokeThickness / 2 : 0;
            padding += circle.Padding;

            // 判断绘制的圆弧是否可见,以提高性能
            double width = circle.Width;
            double height = circle.Height;
            if (double.IsNaN(width) || double.IsNaN(height))
            {
                if (double.IsNaN(circle.DesiredSize.Width) || double.IsNaN(circle.DesiredSize.Height) || circle.DesiredSize == new Size(0, 0))
                {
                    width = circle.ActualWidth;
                    height = circle.ActualHeight;
                }
                else
                {
                    width = circle.DesiredSize.Width;
                    height = circle.DesiredSize.Height;
                }
            }
            if (width > 0 && height > 0 && circle.StartAngle != circle.EndAngle)
            {
                double ellipseA = width / 2 - padding;
                double ellipseB = height / 2 - padding;

                // 起止角度间隔大于等于360°,直接画圆

                if (Math.Abs(circle.StartAngle - circle.EndAngle) >= 360)
                {
                    string data = $"M{padding + ellipseA * 2},{padding + ellipseB + 0.001} A{ellipseA},{ellipseB} 0,1,1 {padding + ellipseA * 2},{padding + ellipseB}z";
                    circle.Data = (Geometry)Geometry.Parse(data);
                    return;
                }

                // 椭圆公式:X²/a²+Y²/b²=1
                // Rect与椭圆各参数的对应关系:
                // Rect.X与Rect.Y分别是椭圆外接矩形相对Circle区域的左上角的偏移量;
                // Rect.Width与Rect.Height分别是椭圆外接矩形的宽和高
                // 以下根据StartAngle和EndAngle算出圆弧在矩形函数曲线中的起始点和终止点

                // 判断绘制方向
                bool clockWise = circle.EndAngle > circle.StartAngle;

                // 判断优弧/劣弧
                bool majorArc = Math.Abs(circle.StartAngle - circle.EndAngle) % 360 >= 180;

                // 将起始点和终止点转化为椭圆上的坐标
                double rad = 0;

                Point startPoint = new Point();
                if ((90 - circle.StartAngle) % 360 == 0)
                {
                    startPoint.X = 0;
                    startPoint.Y = ellipseB;
                }
                else if ((270 - circle.StartAngle) % 360 == 0)
                {
                    startPoint.X = 0;
                    startPoint.Y = -ellipseB;
                }
                else
                {
                    rad = GetRadian(circle.StartAngle);
                    startPoint.X = ellipseA * ellipseB / Math.Sqrt(Math.Pow(ellipseB, 2) + Math.Pow(ellipseA * Math.Tan(rad), 2));
                    startPoint.X *= Math.Cos(rad) > 0 ? 1 : -1;
                    startPoint.Y = startPoint.X * Math.Tan(rad);
                }

                Point endPoint = new Point();
                if ((90 - circle.EndAngle) % 360 == 0)
                {
                    endPoint.X = 0;
                    endPoint.Y = ellipseB;
                }
                else if ((270 - circle.EndAngle) % 360 == 0)
                {
                    endPoint.X = 0;
                    endPoint.Y = -ellipseB;
                }
                else
                {
                    rad = GetRadian(circle.EndAngle);
                    endPoint.X = ellipseA * ellipseB / Math.Sqrt(Math.Pow(ellipseB, 2) + Math.Pow(ellipseA * Math.Tan(rad), 2));
                    endPoint.X *= Math.Cos(rad) > 0 ? 1 : -1;
                    endPoint.Y = endPoint.X * Math.Tan(rad);
                }

                string pathData = $"M";
                if (circle.m_Status == CircleStatus.Pie)
                {
                    pathData += $"{ellipseA + padding},{ellipseB + padding} ";
                }
                pathData += $"{startPoint.X + ellipseA + padding},{startPoint.Y + ellipseB + padding} ";
                pathData += $"A{ellipseA},{ellipseB} ";
                pathData += $"0,{(majorArc ? "1" : "0")},{(clockWise ? "1" : "0")} ";
                pathData += $"{endPoint.X + ellipseA + padding},{endPoint.Y + ellipseB + padding}";
                circle.Data = (Geometry)Geometry.Parse(pathData);
            }
            else
            {
                circle.Data = (Geometry)Geometry.Parse("");
            }
        }
    }

    /// <summary>
    /// 将角度转化为弧度
    /// </summary>
    /// <param name="angle"></param>
    /// <returns></returns>
    private static double GetRadian(double angle)
    {
        return angle / 180.0 * Math.PI;
    }
    #endregion
}

进阶玩法:

只需两个Circle即可构成环形进度条

<Grid Width="70" Height="70">
    <local:Circle StartAngle="0" EndAngle="360" Stroke="LightGray" StrokeThickness="15"/>
    <local:Circle StartAngle="-90" EndAngle="180" Stroke="DodgerBlue" StrokeThickness="15"/>
    <TextBlock Text="75%" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值