しっぽを追いかけて

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

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

円弧シェイプを利用した数値の表現

意外と用意されていない始点と終点角度を指定できる円弧 - しっぽを追いかけて で作った円弧シェイプを使って数字と円弧を合わせて表示するチャート?を作ってみました

f:id:matatabi_ux:20140810115727p:plain

前回の Arc コントロールを利用して ArcChart というユーザーコントロールを下記のように追加します

<UserControl x:Class="ShapeSample.Controls.ArcChart"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:ctrl="using:ShapeSample.Controls"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             FontSize="56"
             FontWeight="Bold"
             Foreground="#FFE3E3E3"
             d:DesignHeight="300"
             d:DesignWidth="400"
             mc:Ignorable="d">
    <UserControl.Resources>
        <Storyboard x:Name="ValueChangeAnimation">
            <DoubleAnimationUsingKeyFrames Duration="0:0:0.7"
                                           EnableDependentAnimation="True"
                                           Storyboard.TargetName="Arc"
                                           Storyboard.TargetProperty="(ctrl:Arc.EndAngle)">
                <DiscreteDoubleKeyFrame KeyTime="0" Value="120" />
                <EasingDoubleKeyFrame KeyTime="0:0:0.7" Value="420">
                    <EasingDoubleKeyFrame.EasingFunction>
                        <QuarticEase EasingMode="EaseOut" />
                    </EasingDoubleKeyFrame.EasingFunction>
                </EasingDoubleKeyFrame>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>
    </UserControl.Resources>

    <Grid>
        <Grid Margin="15,15,0,0"
              HorizontalAlignment="Center"
              VerticalAlignment="Center">
            <ctrl:Arc x:Name="BackgroundArc"
                      EndAngle="420"
                      StartAngle="120"
                      Stroke="#FF1A1A1A"
                      StrokeThickness="30" />
            <ctrl:Arc x:Name="Arc"
                      EndAngle="120"
                      StartAngle="120"
                      StrokeThickness="30" />
        </Grid>
        <TextBlock x:Name="Number"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center" />
    </Grid>
</UserControl>

Arc 2つと TextBlock 2つを配置しただけ

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

/// <summary>
/// 円弧型チャート
/// </summary>
public sealed partial class ArcChart : UserControl
{
    #region Value 依存関係プロパティ
    /// <summary>
    /// Value 依存関係プロパティ
    /// </summary>
    public static readonly DependencyProperty ValueProperty
        = DependencyProperty.Register(
        "Value",
        typeof(double),
        typeof(ArcChart),
        new PropertyMetadata(
            0d,
            (s, e) =>
            {
                var control = s as ArcChart;
                if (control != null)
                {
                    control.OnValueChanged(s, new DoubleValueChangedEventArgs((double)e.NewValue, (double)e.OldValue));
                }
            }));

    /// <summary>
    /// 値
    /// </summary>
    public double Value
    {
        get { return (double)this.GetValue(ValueProperty); }
        set
        {
            if (value > this.MaxValue)
            {
                value = this.MaxValue;
            }
            if (value < this.MinValue)
            {
                value = this.MinValue;
            }
            if (Math.Abs(this.Value - value) > 0)
            {
                this.OnValueChanging(this, new DoubleValueChangedEventArgs(value, this.Value));
            }
            this.SetValue(ValueProperty, value);
        }
    }
    #endregion //Value 依存関係プロパティ

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

    /// <summary>
    /// MaxValue 変更イベントハンドラ
    /// </summary>
    private void OnMaxValueChanged()
    {
    }

    /// <summary>
    /// 最大値
    /// </summary>
    public double MaxValue
    {
        get { return (double)this.GetValue(MaxValueProperty); }
        set { this.SetValue(MaxValueProperty, value); }
    }
    #endregion //MaxValue 依存関係プロパティ

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

    /// <summary>
    /// MinValue 変更イベントハンドラ
    /// </summary>
    private void OnMinValueChanged()
    {
    }

    /// <summary>
    /// 最小値
    /// </summary>
    public double MinValue
    {
        get { return (double)this.GetValue(MinValueProperty); }
        set { this.SetValue(MinValueProperty, value); }
    }
    #endregion //MinValue 依存関係プロパティ

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

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

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

    #region Radius 依存関係プロパティ
    /// <summary>
    /// Radius 依存関係プロパティ
    /// </summary>
    public static readonly DependencyProperty RadiusProperty
        = DependencyProperty.Register(
        "Radius",
        typeof(double),
        typeof(ArcChart),
        new PropertyMetadata(
            100d,
            (s, e) =>
            {
                var control = s as ArcChart;
                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 Stroke 依存関係プロパティ
    /// <summary>
    /// Stroke 依存関係プロパティ
    /// </summary>
    public static readonly DependencyProperty StrokeProperty
        = DependencyProperty.Register(
        "Stroke",
        typeof(Brush),
        typeof(ArcChart),
        new PropertyMetadata(
            new SolidColorBrush(Colors.OrangeRed),
            (s, e) =>
            {
                var control = s as ArcChart;
                if (control != null)
                {
                    control.OnStrokeChanged();
                }
            }));

    /// <summary>
    /// Stroke 変更イベントハンドラ
    /// </summary>
    private void OnStrokeChanged()
    {
        this.Arc.Stroke = this.Stroke;
    }

    /// <summary>
    /// Shape のアウトラインの描画方法を指定する Brush
    /// </summary>
    public Brush Stroke
    {
        get { return (Brush)this.GetValue(StrokeProperty); }
        set { this.SetValue(StrokeProperty, value); }
    }
    #endregion //Stroke 依存関係プロパティ

    /// <summary>
    /// 値変更時イベント
    /// </summary>
    public event DoubleValueChangedEventHandler ValueChanging;

    /// <summary>
    /// 値変更後イベント
    /// </summary>
    public event DoubleValueChangedEventHandler ValueChanged;

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

        this.OnValueChanging(this, new DoubleValueChangedEventArgs(this.Value, this.MinValue));
    }

    /// <summary>
    /// 描画する
    /// </summary>
    public void Render()
    {
        this.BackgroundArc.Radius = this.Radius;
        this.Arc.Radius = this.Radius;
        //this.Arc.EndAngle = this.ComputeAngle(this.Value);
        this.Number.Text = this.Value.ToString();
    }

    /// <summary>
    /// 値から角度を計算する
    /// </summary>
    /// <param name="value"></param>
    /// <returns>角度</returns>
    private double ComputeAngle(double value)
    {
        return 120d + ((value - this.MinValue) / (this.MaxValue - this.MinValue)) * 300d;
    }

    /// <summary>
    /// 値変更時の処理
    /// </summary>
    /// <param name="sender">イベント発行者</param>
    /// <param name="e">イベント引数</param>
    private void OnValueChanging(object sender, DoubleValueChangedEventArgs e)
    {
        if (this.ValueChanging != null)
        {
            this.ValueChanging(this, e);
        }

        var animation = this.ValueChangeAnimation.Children.First() as DoubleAnimationUsingKeyFrames;
        if (animation == null)
        {
            return;
        }
        animation.KeyFrames[0].Value = this.ComputeAngle(e.OldValue);
        animation.KeyFrames[1].Value = this.ComputeAngle(e.NewValue);
        this.ValueChangeAnimation.Begin();
    }

    /// <summary>
    /// 値変更後の処理
    /// </summary>
    /// <param name="sender">イベント発行者</param>
    /// <param name="e">イベント引数</param>
    private void OnValueChanged(object sender, DoubleValueChangedEventArgs e)
    {
        this.Render();

        if (this.ValueChanged != null)
        {
            this.ValueChanged(this, e);
        }
    }
}

/// <summary>
/// ValueChanged イベントを処理するメソッドを表します
/// </summary>
/// <param name="sender">イベント発行者</param>
/// <param name="e">イベントのデータ</param>
public delegate void DoubleValueChangedEventHandler(object sender, DoubleValueChangedEventArgs e);

/// <summary>
/// double 値の変更イベントのデータ
/// </summary>
public class DoubleValueChangedEventArgs : RoutedEventArgs
{
    /// <summary>
    /// 新しい値
    /// </summary>
    public double NewValue { get; private set; }

    /// <summary>
    /// 古い値
    /// </summary>
    public double OldValue { get; private set; }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="newValue">新しい値</param>
    /// <param name="oldValue">古い値</param>
    public DoubleValueChangedEventArgs(double newValue, double oldValue)
    {
        this.NewValue = newValue;
        this.OldValue = oldValue;
    }
}

依存関係プロパティやイベント関係の実装がありますが重要なのは下記の部分

    /// <summary>
    /// 値から角度を計算する
    /// </summary>
    /// <param name="value"></param>
    /// <returns>角度</returns>
    private double ComputeAngle(double value)
    {
        return 120d + ((value - this.MinValue) / (this.MaxValue - this.MinValue)) * 300d;
    }

    /// <summary>
    /// 値変更時の処理
    /// </summary>
    /// <param name="sender">イベント発行者</param>
    /// <param name="e">イベント引数</param>
    private void OnValueChanging(object sender, DoubleValueChangedEventArgs e)
    {
        if (this.ValueChanging != null)
        {
            this.ValueChanging(this, e);
        }

        var animation = this.ValueChangeAnimation.Children.First() as DoubleAnimationUsingKeyFrames;
        if (animation == null)
        {
            return;
        }
        animation.KeyFrames[0].Value = this.ComputeAngle(e.OldValue);
        animation.KeyFrames[1].Value = this.ComputeAngle(e.NewValue);
        this.ValueChangeAnimation.Begin();
    }

値から対応する角度を割り出したり、XAML に定義済みの Storyboard を利用して円弧の増減時のイージングつきアニメを走らせています

実際に動作させてみたのが次の動画


円弧型チャートコントロール? - YouTube

イージングのおかげでにゅるにゅる動きますね