如何使用半径、起始角度和结束角度绘制弧形

17
如果我的Canvas元素的DataContext中有以下四个属性:
Point  Center
double Radius
double StartAngle
double EndAngle

我能否在不使用额外代码的情况下绘制弧线?


也许这个链接对你有用:https://dev59.com/DGw15IYBdhLWcg3wO5SX - dkozl
几乎可以了,但我仍然需要在代码后台或视图模型中手动计算弧线段的起点和终点。我可以做到,但希望不必这样做。 :( - bradgonesurfing
我可能会像这里(https://dev59.com/t2jWa4cB1Zd3GeqPplMn)那样定义一个自定义的弧形形状。 - bradgonesurfing
2个回答

33

提供自定义组件证明是最佳解决方案。在我的代码中,我像这样使用它

<Controls:Arc Center="{Binding Path=PreviousMousePositionPixels}" 
         Stroke="White" 
         StrokeDashArray="4 4"
         SnapsToDevicePixels="True"
         StartAngle="0" 
         EndAngle="{Binding Path=DeltaAngle}" 
         SmallAngle="True"
         Radius="40" />

SmallAngletrue时,无论StartAngleEndAngle的顺序如何,都会呈现出两点之间的小角度。当SmallAnglefalse时,圆弧逆时针渲染。

实现方式为:

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;

public sealed class Arc : Shape
{
    public Point Center
    {
        get => (Point)GetValue(CenterProperty);
        set => SetValue(CenterProperty, value);
    }

    // Using a DependencyProperty as the backing store for Center.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty CenterProperty = 
        DependencyProperty.Register(nameof(Center), typeof(Point), typeof(Arc), 
            new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsRender));

    public double StartAngle
    {
        get => (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(nameof(StartAngle), typeof(double), typeof(Arc),
            new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));

    public double EndAngle
    {
        get => (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(nameof(EndAngle), typeof(double), typeof(Arc),
            new FrameworkPropertyMetadata(Math.PI / 2.0, FrameworkPropertyMetadataOptions.AffectsRender));

    public double Radius
    {
        get => (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(nameof(Radius), typeof(double), typeof(Arc),
            new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender));

    public bool SmallAngle
    {
        get => (bool)GetValue(SmallAngleProperty);
        set => SetValue(SmallAngleProperty, value);
    }

    // Using a DependencyProperty as the backing store for SmallAngle.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SmallAngleProperty =
        DependencyProperty.Register(nameof(SmallAngle), typeof(bool), typeof(Arc),
            new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));

    static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));

    protected override Geometry DefiningGeometry
    {
        get
        {
            double a0 = StartAngle < 0 ? StartAngle + 2 * Math.PI : StartAngle;
            double a1 = EndAngle < 0 ? EndAngle + 2 * Math.PI : EndAngle;

            if (a1 < a0)
                a1 += Math.PI * 2;

            SweepDirection d = SweepDirection.Counterclockwise;
            bool large;

            if (SmallAngle)
            {
                large = false;
                d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;
            }
            else
                large = (Math.Abs(a1 - a0) < Math.PI);

            Point p0 = Center + new Vector(Math.Cos(a0), Math.Sin(a0)) * Radius;
            Point p1 = Center + new Vector(Math.Cos(a1), Math.Sin(a1)) * Radius;

            List<PathSegment> segments = new List<PathSegment>
            {
                new ArcSegment(p1, new Size(Radius, Radius), 0.0, large, d, true)
            };

            List<PathFigure> figures = new List<PathFigure>
            {
                new PathFigure(p0, segments, true)
                {
                    IsClosed = false
                }
            };

            return new PathGeometry(figures, FillRule.EvenOdd, null);
        }
    }
}

1
感谢您抽出时间发布这篇文章。它为我节省了很多时间,用于制作弧形动画。请注意,该形状可以在XAML中使用,使用方法为<local:Arc />。 - michael
这里测量的起始角度和结束角度应该是以弧度为单位。 - Ivan P.
那是正确的。 - bradgonesurfing

7
我可以提供一个稍微不同的解决方案吗?
class ArcII:FrameworkElement
{
    /// <summary>
    /// Center point of Arc.
    /// </summary>
    [Category("Arc")]
    public Point Center
    {
        get { return (Point)GetValue(CenterProperty); }
        set { SetValue(CenterProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Center.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty CenterProperty =
        DependencyProperty.Register("Center", typeof(Point), typeof(ArcII), new FrameworkPropertyMetadata(new Point(0, 0), FrameworkPropertyMetadataOptions.AffectsRender));
    
    /// <summary>
    /// Forces the Arc to the center of the Parent container.
    /// </summary>
    [Category("Arc")]
    public bool OverrideCenter
    {
        get { return (bool)GetValue(OverrideCenterProperty); }
        set { SetValue(OverrideCenterProperty, value); }
    }

    // Using a DependencyProperty as the backing store for OverrideCenter.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty OverrideCenterProperty =
        DependencyProperty.Register("OverrideCenter", typeof(bool), typeof(ArcII), new FrameworkPropertyMetadata((bool)false, FrameworkPropertyMetadataOptions.AffectsRender));

    /// <summary>
    /// Start angle of arc, using standard coordinates. (Zero is right, CCW positive direction)
    /// </summary>
    [Category("Arc")]
    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(ArcII), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));

    /// <summary>
    /// Length of Arc in degrees.
    /// </summary>
    [Category("Arc")]
    public double SweepAngle
    {
        get { return (double)GetValue(SweepAngleProperty); }
        set { SetValue(SweepAngleProperty, value); }
    }

    // Using a DependencyProperty as the backing store for SweepAngle.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SweepAngleProperty =
        DependencyProperty.Register("SweepAngle", typeof(double), typeof(ArcII), new FrameworkPropertyMetadata((double)180, FrameworkPropertyMetadataOptions.AffectsRender));

    /// <summary>
    /// Size of Arc.
    /// </summary>
    [Category("Arc")]
    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(ArcII), new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender));

    [Category("Arc")]
    public Brush Stroke
    {
        get { return (Brush)GetValue(StrokeProperty); }
        set { SetValue(StrokeProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Stroke.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty StrokeProperty =
        DependencyProperty.Register("Stroke", typeof(Brush), typeof(ArcII), new FrameworkPropertyMetadata((Brush)Brushes.Black,FrameworkPropertyMetadataOptions.AffectsRender));

    [Category("Arc")]
    public double StrokeThickness
    {
        get { return (double)GetValue(StrokeThicknessProperty); }
        set { SetValue(StrokeThicknessProperty, value); }
    }

    // Using a DependencyProperty as the backing store for StrokeThickness.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty StrokeThicknessProperty =
        DependencyProperty.Register("StrokeThickness", typeof(double), typeof(ArcII), new FrameworkPropertyMetadata((double)1,FrameworkPropertyMetadataOptions.AffectsRender));

    protected override void OnRender(DrawingContext dc)
    {
        base.OnRender(dc);
        Draw(dc);
    }

    private void Draw(DrawingContext dc)
    {
        Point center = new Point();
        if (OverrideCenter)
        {
            Rect rect = new Rect(RenderSize);
            center = Polar.CenterPointFromRect(rect);
        }
        else
        {
            center = Center;
        }

        Point startPoint = Polar.PolarToCartesian(StartAngle, Radius, center);
        Point endPoint = Polar.PolarToCartesian(StartAngle + SweepAngle, Radius, center);
        Size size = new Size(Radius, Radius);

        bool isLarge = (StartAngle + SweepAngle) - StartAngle > 180;

        List<PathSegment> segments = new List<PathSegment>(1);
        segments.Add(new ArcSegment(endPoint, new Size(Radius, Radius), 0.0, isLarge, SweepDirection.Clockwise, true));

        List<PathFigure> figures = new List<PathFigure>(1);
        PathFigure pf = new PathFigure(startPoint, segments, true);
        pf.IsClosed = false;
        figures.Add(pf);
        Geometry g = new PathGeometry(figures, FillRule.EvenOdd, null);

        dc.DrawGeometry(null, new Pen(Stroke,StrokeThickness), g);
    }
}

使用方法:

    <!--Centerd on Parent-->
    <local:ArcII Center="0,0"
                 OverrideCenter="True"
                 StartAngle="150"
                 SweepAngle="240"
                 Radius="100"
                 Stroke="Red"
                 StrokeThickness="3"
                 />

    <!--Centerd on Parent-->
    <local:ArcII Center="0,0"
                 OverrideCenter="True"
                 StartAngle="150"
                 SweepAngle="240"
                 Radius="95"
                 Stroke="Red"
                 StrokeThickness="3"
                 />

    <!--Centerd on Parent-->
    <local:ArcII Center="0,0"
                 OverrideCenter="True"
                 StartAngle="150"
                 SweepAngle="240"
                 Radius="90"
                 Stroke="Red"
                 StrokeThickness="3"
                 />

    <!--Centerd on Point-->
    <local:ArcII Center="0,150"
                 OverrideCenter="False"
                 StartAngle="270"
                 SweepAngle="180"
                 Radius="100"
                 />

    <!--Centerd on Point-->
    <local:ArcII Center="525,150"
                 OverrideCenter="False"
                 StartAngle="90"
                 SweepAngle="180"
                 Radius="100"
                 />

注意: A) 这不会做出一个360度SweepAngle,要做到这一点请使用椭圆。 B) OverrideCenter: 这将把弧的中心放在其父元素的中心。请注意,像网格这样可以分区的元素仍然有一个中心,这个中心可能不是Arc所在的列或行。

抱歉,这里已经有一段时间了。更新提供极坐标类...

public static class Polar
{
    /// <summary>
    /// Given the center of a circle and its radius, along with the angle 
    /// corresponding to the point, find the coordinates.  In other words, 
    /// convert from polar to rectangular coordinates.
    /// </summary>
    /// <param name="angle"></param>
    /// <param name="radius"></param>
    /// <param name="center"></param>
    /// <returns></returns>
    public static Point PolarToCartesian(double angle, double radius, Point center)
    {
        return new Point((center.X + (radius * Math.Cos(DegreesToRadian(angle)))), (center.Y + (radius * Math.Sin(DegreesToRadian(angle)))));
    }


    /// <summary>
    /// Given a center point and radius, find the top left point for a rectangle and its size.
    /// </summary>
    /// <param name="centerPoint"></param>
    /// <param name="radius"></param>
    /// <returns></returns>
    public static Rect RectFromCenterPoint(Point centerPoint, int radius)
    {
        Point p = new Point(centerPoint.X - radius, centerPoint.Y - radius);
        return new Rect(p, new Size(radius * 2, radius * 2));
    }

    /// <summary>
    /// Finds the center point of a Rect
    /// </summary>
    /// <param name="rect"></param>
    /// <returns></returns>
    public static Point CenterPoint(Rect rect)
    {
        return new Point(rect.Width / 2, rect.Height / 2);
    }

    /// <summary>
    /// Returns a radius value equal to the smallest side.
    /// </summary>
    /// <param name="rect"></param>
    /// <returns></returns>
    public static double Radius(Rect rect)
    {
        double dbl = Math.Min(rect.Width, rect.Height);
        return dbl / 2;
    }


    /// <summary>
    /// Since Windows Forms consider an Angle of Zero to be at the 3:00 position and an Angle of 90
    /// to be at the 12:00 position, it is sometimes difficult to visualize where 
    /// 
    /// </summary>
    /// <param name="Angle"></param>
    /// <param name="Offset"></param>
    /// <returns></returns>
    /// <remarks></remarks>
    public static float ReversePolarDirection(float Angle, int Offset)
    {
        return ((360 - Angle) + Offset) % 360;
    }

    /// <summary>
    /// Circumference: C = 2*Pi*r = Pi*d; r=Radius, d=Diameter
    /// </summary>
    /// <param name="Diameter"></param>
    /// <returns></returns>
    /// <remarks></remarks>
    public static double CircumferenceD(double Diameter)
    {
        return Diameter * Math.PI;
    }
    /// <summary>
    /// Circumference: C = 2*Pi*r = Pi*d; r=Radius, d=Diameter
    /// </summary>
    /// <param name="Radius"></param>
    /// <returns></returns>
    /// <remarks></remarks>
    public static double CircumferenceR(double Radius)
    {
        return Radius * Math.PI;
    }
    public static double ScaleWithParam(double Input, double InputMin, double InputMax, double ScaledMin, double ScaledMax)
    {
        //Out = (((ScMax-ScMin)/(InMax-InMin))*Input)+(ScMin-(InMin*((ScMax-ScMin)/(InMax-InMin))
        return (((ScaledMax - ScaledMin) / (InputMax - InputMin)) * Input) + (ScaledMin - (InputMin * ((ScaledMax - ScaledMin) / (InputMax - InputMin))));

    }
    public static double DegreesToRadian(double degrees)
    {
        //Return 2 * Math.PI * degrees / 360.0
        return degrees * (Math.PI / 180);
    }
    private static double RadianToDegrees(double radian)
    {
        return radian * 180 / Math.PI;
    }

    public static double ArcLength(double radius, double radian)
    {
        return radius * radian;
    }
}

2
这行代码:bool isLarge = (StartAngle + SweepAngle) - StartAngle > 180; 看起来可能可以稍微优化一下 ;) - Richard Irons
Polar.PolarToCartesian 属于哪个命名空间? - metoyou
更新答案以包括极坐标类。 - Brian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接