de-vraag
  • 質問
  • タグ
  • ユーザー
通知:
報酬:
登録
登録すると、質問に対する返答やコメントが通知されます。
ログイン
すでにアカウントをお持ちの方は、ログインして新しい通知を確認してください。
追加された質問、回答、コメントには報酬があります。
さらに
ソース
編集
ゲストユーザ
質問

円弧アニメーションによるカウントダウン制御

私はこのように見えるアークアニメーションでカウントダウンタイマーコントロールを実装しました

countdown control


実施上の注意:

  • For arc visualization I've created class <コード>Arc derived from <コード>Shape (コード is based on this post).

  • I've created <コード>Countdown control (derived from <コード>UserControl). For setting timeout I've added <コード>Seconds dependency property. I'm using binding <コード>Content="{Binding Seconds}" to display seconds. Animation duration is set in コード behind

    <コード>Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
    

    because I'm not sure if it's possible to do it in XAML without writing custom converter. I think that creating custom converter is not justified here.

  • For control's scaling content is wrapped in <コード>Viewbox сontrol.

  • For seconds animation I'm using <コード>DispatcherTimer, nothing special. Is it the best to go here?


コード

<コード>Arc.cs

<コード>public 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("Center", typeof(Point), typeof(Arc), 
            new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsRender));

   //Start angle in degrees
    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("StartAngle", typeof(double), typeof(Arc),
            new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));

   //End angle in degrees
    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("EndAngle", typeof(double), typeof(Arc),
            new FrameworkPropertyMetadata(90.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("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("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 startAngleRadians = StartAngle * Math.PI/180;
            double endAngleRadians = EndAngle * Math.PI/180;

            double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
            double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;

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

            SweepDirection d = SweepDirection.Counterclockwise;
            bool large;

            if (SmallAngle)
            {
                large = false;
                double t = a1;
                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);
        }
    }
}

<コード>Countdown.xaml

<コード>
    
        
            
                
                    
                
            
        
    
    
        
            
                
                    
            

            
        
    

<コード>Countdown.xaml.cs

<コード>public partial class Countdown : UserControl
{
    public int Seconds
    {
        get => (int)GetValue(SecondsProperty);
        set => SetValue(SecondsProperty, value);
    }

    public static readonly DependencyProperty SecondsProperty =
        DependencyProperty.Register(nameof(Seconds), typeof(int), typeof(Countdown), new PropertyMetadata(0));

    private readonly DispatcherTimer _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };

    public Countdown()
    {
        InitializeComponent();
        DataContext = this;
    }

    private void UserControl_Loaded(object sender, EventArgs e)
    {
        Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
        if (Seconds > 0)
        {
            _timer.Start();
            _timer.Tick += Timer_Tick;
        }
    }

    private void Timer_Tick(object sender, EventArgs e)
    {
        Seconds--;
        if (Seconds == 0) _timer.Stop();
    }
}

Control is placed on <コード>Window like this

<コード>
11 2018-06-25T08:48:43+00:00 4
コードレビュー
animation
c#
timer
wpf
Matthew Leingang
25日 6月 2018 в 10:22
2018-06-25T10:22:40+00:00
さらに
ソース
編集
#84965761

もっと一貫性のあるものがいくつかあります。

  public static readonly DependencyProperty StartAngleProperty =
        DependencyProperty.Register( "StartAngle"、typeof(double)、typeof(Arc)、
            new FrameworkPropertyMetadata(0.0、FrameworkPropertyMetadataOptions.AffectsRender));
 
  public static readonly DependencyProperty SecondsProperty =
        DependencyProperty.Register(nameof(Seconds)、typeof(int)、typeof(Countdown)、新しいPropertyMetadata(0));
 

Countdown.xaml.cs の依存関係プロパティは nameof を使用しますが、 Arc.cs のものは使用しません。 IMOは彼らがすべきです。


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

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

Arc.cs の依存関係プロパティにはすべてコメントがありますが、コンストラクタにはありません。 IMOのコンストラクタは理解するのが難しいです。コメントなしでそれを理解するのに十分なWPFを誰かが知っている場合、依存関係プロパティに関するコメントは必要ありません。


            double startAngleRadians = StartAngle * Math.PI/180;
            double endAngleRadians = EndAngle * Math.PI/180;

            double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
            double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;

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

            SweepDirection d = SweepDirection.Counterclockwise;
            bool large;

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

ここで何が起こっているのかを説明するために私は間違いなくいくつかのコメントを使用することができます。特に最後の行は非常に直感に反するように見えます。

デッドラインもあります。 t はどこにも使用されていません。


                    

ローカライズできません...


  _timer.Start();
            _timer.Tick + = Timer_Tick;
 

実際には起こりそうもないが、技術的には 競合状態になっている。 2行の順番を入れ替えることには利点があるだけです。


...タイムアウトを設定するためにSeconds依存プロパティを追加しました。秒を表示するためにbinding Content = "{Binding Seconds}" を使用しています。アニメーションの長さは   背後にあるコードで設定

  Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
 
     

何も書かずにXAMLでそれが可能かどうかわからないので   カスタムコンバータカスタムコンバータを作成することはできないと思います   ここで正当化した。

バインディングプロパティを TimeSpan ( Seconds からのようなものに変更する)にすることで、カスタムコンバータなしでそれができると思います 。 TimeRemaining )とラベルコンテンツのバインディングでの StringFormat の使用私はこれをテストしていません。

これを行うならば、あなたはセッターの最も近い秒に丸めることによって非整数の TimeSpan.TotalSeconds 値を処理したいかもしれません。

10
0
yogman
25日 6月 2018 в 11:28
2018-06-25T11:28:11+00:00
さらに
ソース
編集
#84965762

1) DispatcherTimer は定期的なUIの更新には適していますが、正確さが不十分なため、時間の測定には使用しないでください。正確な時間を測定するには、タイマーコールバック内で Stopwatch クラスを使用する必要があります。

2) Seconds プロパティは明らかに2つのことをします:それは "countdown duration"として始まりますが、コントロールがロードされた後は "残り時間"として振舞います。ここでは2つのプロパティを使用して、必要に応じてそれらを別々にデータバインドできるようにします。

3) DataContext = this; - 再利用可能な公開型に DataContext を設定しないでください。誰かが(あなた自身を含めて)MVVM環境でこのクラスを使うことを最終的に決めることができますが、あなたのクラスの DataContext を変更することはそれを壊すでしょう。内部では、 Binding クラスの RelativeSource または ElementName プロパティ、または DependencyProperty コールバックを使用して入札を行います DataContext が空です。

これが ElementName の使用例です。



 ...
         
         
7
0
Shilesh Babu
25日 6月 2018 в 2:11
2018-06-25T14:11:29+00:00
さらに
ソース
編集
#84965763

2つのタイマー/タイムラインを同時に実行しているのは嫌いです。代わりに、コードビハインドからアニメーションを実行することができます。それはまたあなたにload以外の場所からそれを引き起こす機会を与えます:

private void StartAnimation()
{
  double from = -90;
  double to = 270;
  int seconds = Seconds;
  TimeSpan duration = TimeSpan.FromSeconds(Seconds);

  DoubleAnimation animation = new DoubleAnimation(from, to, new Duration(duration));

  Storyboard.SetTarget(animation, Arc);
  Storyboard.SetTargetProperty(animation, new PropertyPath("EndAngle"));

  Storyboard storyboard = new Storyboard();
  storyboard.CurrentTimeInvalidated += (s, e) =>
  {
    int diff = (int)((s as ClockGroup).CurrentTime.Value.TotalSeconds); 
    Seconds = seconds - diff;
  };

  storyboard.Children.Add(animation);
  storyboard.Begin();
}

private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
  StartAnimation();
  //Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
  //if (Seconds > 0)
  //{
 // _timer.Tick += Timer_Tick;
 // _timer.Start();
  //}
}


  
    
      
        
          
      

      
    
  

3
0
Vadim Ovchinnikov
25日 6月 2018 в 9:25
2018-06-25T21:25:51+00:00
さらに
ソース
編集
#84965764

私のソリューションに対するほぼすべての推奨事項を取り入れましたが、最も過激なものは承認された回答です。どんなフィードバックでも大歓迎です!

Start と Stop のパブリックメソッドと Elapsed イベントも追加しました。

結果:

Arc.cs

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

    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);
    }

    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);
    }

    public static readonly DependencyProperty EndAngleProperty =
        DependencyProperty.Register(nameof(EndAngle), typeof(double), typeof(Arc),
            new FrameworkPropertyMetadata(90.0, FrameworkPropertyMetadataOptions.AffectsRender));

    public double Radius
    {
        get => (double)GetValue(RadiusProperty);
        set => SetValue(RadiusProperty, value);
    }

    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);
    }

    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 startAngleRadians = StartAngle * Math.PI/180;
            double endAngleRadians = EndAngle * Math.PI/180;

            double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
            double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;

            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);
        }
    }
}

Countdown.xaml


    
        
            
                
                    
            

            
        
    

Countdown.xaml.cs

public partial class Countdown : UserControl
{
    public Duration Duration
    {
        get => (Duration)GetValue(DurationProperty);
        set => SetValue(DurationProperty, value);
    }

    public static readonly DependencyProperty DurationProperty =
        DependencyProperty.Register(nameof(Duration), typeof(Duration), typeof(Countdown), new PropertyMetadata(new Duration()));

    public int SecondsRemaining
    {
        get => (int)GetValue(SecondsRemainingProperty);
        set => SetValue(SecondsRemainingProperty, value);
    }

    public static readonly DependencyProperty SecondsRemainingProperty =
        DependencyProperty.Register(nameof(SecondsRemaining), typeof(int), typeof(Countdown), new PropertyMetadata(0));

    public event EventHandler Elapsed;

    private readonly Storyboard _storyboard = new Storyboard();

    public Countdown()
    {
        InitializeComponent();

        DoubleAnimation animation = new DoubleAnimation(-90, 270, Duration);
        Storyboard.SetTarget(animation, Arc);
        Storyboard.SetTargetProperty(animation, new PropertyPath(nameof(Arc.EndAngle)));
        _storyboard.Children.Add(animation);

        DataContext = this;
    }

    private void Countdown_Loaded(object sender, EventArgs e)
    {
        if (IsVisible)
            Start();
    }

    public void Start()
    {
        Stop();

        _storyboard.CurrentTimeInvalidated += Storyboard_CurrentTimeInvalidated;
        _storyboard.Completed += Storyboard_Completed;

        _storyboard.Begin();
    }

    public void Stop()
    {
        _storyboard.CurrentTimeInvalidated -= Storyboard_CurrentTimeInvalidated;
        _storyboard.Completed -= Storyboard_Completed;

        _storyboard.Stop();
    }

    private void Storyboard_CurrentTimeInvalidated(object sender, EventArgs e)
    {
        ClockGroup cg = (ClockGroup)sender;
        if (cg.CurrentTime == null) return;
        TimeSpan elapsedTime = cg.CurrentTime.Value;
        SecondsRemaining = (int)Math.Ceiling((Duration.TimeSpan - elapsedTime).TotalSeconds);
    }

    private void Storyboard_Completed(object sender, EventArgs e)
    {
        if (IsVisible)
            Elapsed?.Invoke(this, EventArgs.Empty);
    }
}

使用例


3
0
質問の追加
カテゴリ
すべて
技術情報
文化・レクリエーション
生活・芸術
科学
プロフェッショナル
事業内容
ユーザー
すべて
新しい
人気
1
Денис Анненский
登録済み 2日前
2
365
登録済み 6日前
3
True Image
登録済み 1週間前
4
archana agarwal
登録済み 1週間前
5
Maxim Zhilyaev
登録済み 1週間前
© de-vraag :年
ソース
codereview.stackexchange.com
ライセンス cc by-sa 3.0 帰属