しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

意外と用意されていない始点と終点角度を指定できる円弧

WPF を含め XAML には XY 座標系で始点終点を指定して描く円弧の Path はありますが、始点終点を角度で指定する円弧がありません・・・

ないものは・・・作ってみました!

<UserControl x:Class="ShapeSample.Controls.Arc"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="using:ShapeSample.Controls"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d">

    <Path x:Name="Shape"/>
</UserControl>

まずは XAML、ユーザーコントロールを作成して上記のように Path を追加しただけ

Path.Data プロパティを隠蔽したいので Path を継承したクラスではなくユーザーコントロールにしてコンポジションにします

コードビハインドの中身は次のように記述します

/// <summary>
/// 角度指定できる円弧シェイプ
/// </summary>
public sealed partial class Arc : UserControl
{
    #region StartAngle 依存関係プロパティ
    /// <summary>
    /// StartAngle 依存関係プロパティ
    /// </summary>
    public static readonly DependencyProperty StartAngleProperty
        = DependencyProperty.Register(
        "StartAngle",
        typeof(double),
        typeof(Arc),
        new PropertyMetadata(
            0d,
            (s, e) =>
            {
                var control = s as Arc;
                if (control != null)
                {
                    control.OnStartAngleChanged();
                }
            }));

    /// <summary>
    /// StartAngle 変更イベントハンドラ
    /// </summary>
    private void OnStartAngleChanged()
    {
        this.Render();
    }

    /// <summary>
    /// 始点角度
    /// </summary>
    public double StartAngle
    {
        get { return (double)this.GetValue(StartAngleProperty); }
        set { this.SetValue(StartAngleProperty, value); }
    }
    #endregion //StartAngle 依存関係プロパティ

    #region EndAngle 依存関係プロパティ
    /// <summary>
    /// EndAngle 依存関係プロパティ
    /// </summary>
    public static readonly DependencyProperty EndAngleProperty
        = DependencyProperty.Register(
        "EndAngle",
        typeof(double),
        typeof(Arc),
        new PropertyMetadata(
            360d,
            (s, e) =>
            {
                var control = s as Arc;
                if (control != null)
                {
                    control.OnEndAngleChanged();
                }
            }));

    /// <summary>
    /// EndAngle 変更イベントハンドラ
    /// </summary>
    private void OnEndAngleChanged()
    {
        this.Render();
    }

    /// <summary>
    /// 終点角度
    /// </summary>
    public double EndAngle
    {
        get { return (double)this.GetValue(EndAngleProperty); }
        set { this.SetValue(EndAngleProperty, value); }
    }
    #endregion //EndAngle 依存関係プロパティ

    #region Radius 依存関係プロパティ
    /// <summary>
    /// Radius 依存関係プロパティ
    /// </summary>
    public static readonly DependencyProperty RadiusProperty
        = DependencyProperty.Register(
        "Radius",
        typeof(double),
        typeof(Arc),
        new PropertyMetadata(
            100d,
            (s, e) =>
            {
                var control = s as Arc;
                if (control != null)
                {
                    control.OnRadiusChanged();
                }
            }));

    /// <summary>
    /// Radius 変更イベントハンドラ
    /// </summary>
    private void OnRadiusChanged()
    {
        this.Render();
    }

    /// <summary>
    /// 半径
    /// </summary>
    public double Radius
    {
        get { return (double)this.GetValue(RadiusProperty); }
        set { this.SetValue(RadiusProperty, value); }
    }
    #endregion //Radius 依存関係プロパティ

    #region StrokeThickness 依存関係プロパティ
    /// <summary>
    /// StrokeThickness 依存関係プロパティ
    /// </summary>
    public static readonly DependencyProperty StrokeThicknessProperty
        = DependencyProperty.Register(
        "StrokeThickness",
        typeof(double),
        typeof(Arc),
        new PropertyMetadata(
            1d,
            (s, e) =>
            {
                var control = s as Arc;
                if (control != null)
                {
                    control.OnStrokeThicknessChanged();
                }
            }));

    /// <summary>
    /// StrokeThickness 変更イベントハンドラ
    /// </summary>
    private void OnStrokeThicknessChanged()
    {
        this.Shape.StrokeThickness = this.StrokeThickness;
    }

    /// <summary>
    /// 枠線の太さ
    /// </summary>
    public double StrokeThickness
    {
        get { return (double)this.GetValue(StrokeThicknessProperty); }
        set { this.SetValue(StrokeThicknessProperty, value); }
    }
    #endregion //StrokeThickness 依存関係プロパティ

    ~ 以下 IShape の依存関係プロパティは省略 ~

    #region StrokeStartLineCap 依存関係プロパティ
    #endregion //StrokeStartLineCap 依存関係

    #region StrokeMiterLimit 依存関係プロパティ
    #endregion //StrokeMiterLimit 依存関係プロパティ

    #region StrokeLineJoin 依存関係プロパティ
    #endregion //StrokeLineJoin 依存関係プロパティ

    #region StrokeEndLineCap 依存関係プロパティ
    #endregion //StrokeEndLineCap 依存関係プロパティ

    #region StrokeDashOffset 依存関係プロパティ
    #endregion //StrokeDashOffset 依存関係プロパティ

    #region StrokeDashCap 依存関係プロパティ
    #endregion //StrokeDashCap 依存関係プロパティ

    #region StrokeDashArray 依存関係プロパティ
    #endregion //StrokeDashCap 依存関係プロパティ

    #region Stroke 依存関係プロパティ
    #endregion //Stroke 依存関係プロパティ

    #region Fill 依存関係プロパティ
    #endregion //Stretch 依存関係プロパティ

    #region Fill 依存関係プロパティ
    #endregion //Fill 依存関係プロパティ

    #region GeometryTransform 依存関係プロパティ
    #endregion //GeometryTransform 依存関係プロパティ

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public Arc()
    {
        this.InitializeComponent();
    }

    /// <summary>
    /// 描画する
    /// </summary>
    public void Render()
    {
        var start = this.StartAngle;
        var end = this.EndAngle;

        if (this.StartAngle > this.EndAngle)
        {
            start = this.EndAngle;
            end = this.StartAngle;
        }

        var figure = new PathFigure();
        figure.IsClosed = false;
        figure.StartPoint = this.ComputeAngle(start);
        for (double i = start + 1; i < end; i++)
        {
            figure.Segments.Add(new ArcSegment()
            {
                IsLargeArc = false,
                RotationAngle = 0,
                Size = new Size(this.Radius, this.Radius),
                Point = this.ComputeAngle(i),
                SweepDirection = SweepDirection.Clockwise,
            });
        }
        if (Math.Floor(this.EndAngle) < this.EndAngle)
        {
            figure.Segments.Add(new ArcSegment()
            {
                IsLargeArc = false,
                RotationAngle = 0,
                Size = new Size(this.Radius, this.Radius),
                Point = this.ComputeAngle(this.EndAngle),
                SweepDirection = SweepDirection.Clockwise,
            });
        }
        var geometory = new PathGeometry();
        geometory.Figures.Add(figure);
        this.Shape.Data = geometory;
    }

    /// <summary>
    /// 角度を XY 座標に変換する
    /// </summary>
    /// <param name="angle">角度</param>
    /// <returns>XY 座標</returns>
    private Point ComputeAngle(double angle)
    {
        return new Point(this.Radius + (this.Radius * Math.Cos(angle * Math.PI / 180)), this.Radius + (this.Radius * Math.Sin(angle * Math.PI / 180)));
    }
}

始点・終点の角度と半径のプロパティを追加し、残りは Shape から継承したいプロパティを追加しました

始点から終点までを一気に Path で描画することができないので、角度1度ずつ ArcSegment をつないで描画するという愚直な方法です

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <ctrl:Arc HorizontalAlignment="Center"
                  VerticalAlignment="Center"
                  EndAngle="150"
                  Radius="100"
                  StartAngle="-60"
                  Stroke="#FFB25920"
                  StrokeThickness="30" />
    </Grid>

さっそく画面の XAML に上記のように記述して実行してみると

f:id:matatabi_ux:20140810001347p:plain

できました!円グラフとかにも応用できそう