在WPF中创建高级节拍器(代码创建动画和完成事件的问题)

时间:2012-06-28 17:15:46

标签: c# wpf animation timer

下午好,

在过去的几周里,我一直致力于一个创建高级节拍器的项目。节拍器由以下内容组成

  1. 一个摆臂
  2. 闪光灯
  3. 动态创建的用户控件的集合,表示节拍(其中4个打开,重音或关闭)。
  4. 显示LCD数字显示的用户控件,并计算所选BPM(60000 / BPM =毫秒)的节拍之间的毫秒数
  5. 用户选择BPM并按下开始,然后发生以下情况

    1. 手臂在两个角度之间以每次扫描n毫秒的速率摆动
    2. 每次手臂扫描结束时灯闪烁
    3. 创建指标并按顺序闪烁(每次扫描结束时一个)。
    4. 现在问题 Arm和light flash动画是在代码中创建的,并添加到故事板中,永远重复并自动反转。

      指标是在代码中创建的,需要在每个Arm扫描动画结束时触发事件。

      所以,经过多次搞乱之后我做的就是创建一个与故事板同步运行的计时器。

      问题是,30秒以上的计时器和故事板不同步,因此指示器和手臂扫描不及时(对于节拍器来说不好!!)。

      我试图抓住已完成的动画事件并将其作为触发器来停止并重新启动计时器,这就是我能想到的所有内容,以保持两者完美同步。

      移动同步是由于故事板滑动以及在使用.start调用计时器之前在线上调用故事板的事实引起的,这虽然微秒我认为意味着它们开始不可靠但不是完全相同的时间。

      我的问题, 当我尝试绑定到完成的动画事件时,它永远不会触发。我的印象是,无论自动反转(即每次迭代之间)都完成了甚至火灾。是不是这样?

      任何人都可以想到另一种(更狡猾)的方式来保持两件事情同步。

      最后,我确实想看看我是否可以从故事板中解雇一个方法(这会让我的生活变得非常轻松,但是看起来似乎无法做到这一点。)

      如果有任何建议我并不珍贵,我只想完成这件事!!

      最终兴趣点, 在节拍器运行时可以调节bpm,这可以通过计算飞行中的毫秒持续时间(鼠标按下按钮)并通过当前速度和新速度之间的差异来缩放故事板来实现。显然,必须同时更改运行指示器的计时器(使用间隔)。

      下面的代码来自我的项目到目前为止(不是XAML只是C#)

      using System;
      using System.Collections.Generic;
      using System.Windows;
      using System.Windows.Input;
      using System.Windows.Media.Animation;
      using System.Windows.Media;
      using System.Windows.Media.Imaging;
      using System.Windows.Controls;
      using System.Windows.Threading;    
      
      namespace MetronomeLibrary
      {    
          public partial class MetronomeLarge
          {
              private bool Running;
      
              //Speed and time signature
              private int _bpm = 60;
              private int _beats = 4;
              private int _beatUnit = 4;
              private int _currentBeat = 1;
              private readonly int _baseSpeed = 60000 / 60;
              private readonly DispatcherTimer BeatTimer = new DispatcherTimer();
      
              private Storyboard storyboard = new Storyboard();
      
              public MetronomeLarge()
              {
                  InitializeComponent();
      
                  NumericDisplay.Value = BPM;
      
                  BeatTimer.Tick += new EventHandler(TimerTick);
      
                  SetUpAnimation();    
                  SetUpIndicators(); 
              }
      
              public int Beats
              {
                  get
                  {
                      return _beats;
                  }
                  set
                  {
                      _beats = value;
                      SetUpIndicators();
                  }
              }
      
              public int BPM
              {
                  get
                  {
                      return _bpm;
                  }
                  set
                  {
                      _bpm = value;
                      //Scale the story board here
                      SetSpeedRatio();
                  }
              }
      
              public int BeatUnit
              {
                  get
                  {
                      return _beatUnit;
                  }
                  set
                  {
                      _beatUnit = value;
                  }
              }
      
              private void SetSpeedRatio()
              {
                  //divide the new speed (bpm by the old speed to get the new ratio)
                  float newMilliseconds = (60000 / BPM);
                  float newRatio = _baseSpeed / newMilliseconds;
                  storyboard.SetSpeedRatio(newRatio);
      
                  //Set the beat timer according to the beattype (standard is quarter beats for one sweep of the metronome
                  BeatTimer.Interval = TimeSpan.FromMilliseconds(newMilliseconds);
              }
      
              private void TimerTick(object sender, EventArgs e)
              {
                  MetronomeBeat(_currentBeat);
      
                  _currentBeat++;
      
                  if (_currentBeat > Beats)
                  {
                      _currentBeat = 1;
                  }
              }
      
              private void MetronomeBeat(int Beat)
              {
                      //turnoff all indicators
                      TurnOffAllIndicators();
      
                      //Find a control by name
                      MetronomeLargeIndicator theIndicator = (MetronomeLargeIndicator)gridContainer.Children[Beat-1];
      
                      //illuminate the control
                      theIndicator.TurnOn();
                      theIndicator.PlaySound();    
              }
      
              private void TurnOffAllIndicators()
              {
      
                  for (int i = 0; i <= gridContainer.Children.Count-1; i++)
                  {
                      MetronomeLargeIndicator theIndicator = (MetronomeLargeIndicator)gridContainer.Children[i];
                      theIndicator.TurnOff();
                  }
              }
      
              private void SetUpIndicators()
              {
                  gridContainer.Children.Clear();
                  gridContainer.ColumnDefinitions.Clear();
      
                  for (int i = 1; i <= _beats; i++)
                  {
                      MetronomeLargeIndicator theNewIndicator = new MetronomeLargeIndicator();
      
                      ColumnDefinition newCol = new ColumnDefinition() { Width = GridLength.Auto };
                      gridContainer.ColumnDefinitions.Add(newCol);
                      gridContainer.Children.Add(theNewIndicator);
                      theNewIndicator.Name = "Indicator" + i.ToString();
                      Grid.SetColumn(theNewIndicator, i - 1);
                  }
              }   
      
              private void DisplayOverlay_MouseDown(object sender, MouseButtonEventArgs e)
              {
                  ToggleAnimation();
              }
      
              private void ToggleAnimation()
              {
                  if (Running)
                  {
                      //stop the animation
                      ((Storyboard)Resources["Storyboard"]).Stop() ;
                      BeatTimer.Stop();
                  }
                  else
                  {
                      //start the animation
                      BeatTimer.Start();
                      ((Storyboard)Resources["Storyboard"]).Begin();
                      SetSpeedRatio();                 
                  }
      
                  Running = !Running;
              }
      
      
              private void ButtonIncrement_Click(object sender, RoutedEventArgs e)
              {
                  NumericDisplay.Value++;
                  BPM = NumericDisplay.Value;
              }
      
              private void ButtonDecrement_Click(object sender, RoutedEventArgs e)
              {
                  NumericDisplay.Value--;
                  BPM = NumericDisplay.Value;
              }
      
              private void ButtonIncrement_MouseEnter(object sender, MouseEventArgs e)
              {
                  ImageBrush theBrush = new ImageBrush() 
                  { 
                      ImageSource = new BitmapImage(new 
                          Uri(@"pack://application:,,,/MetronomeLibrary;component/Images/pad-metronome-increment-button-over.png")) 
                  };
                  ButtonIncrement.Background = theBrush;
              }
      
              private void ButtonIncrement_MouseLeave(object sender, MouseEventArgs e)
              {
                  ImageBrush theBrush = new ImageBrush() 
                  { 
                      ImageSource = new BitmapImage(new 
                          Uri(@"pack://application:,,,/MetronomeLibrary;component/Images/pad-metronome-increment-button.png")) 
                  };
                  ButtonIncrement.Background = theBrush;
              }
      
              private void ButtonDecrement_MouseEnter(object sender, MouseEventArgs e)
              {
                  ImageBrush theBrush = new ImageBrush() 
                  { 
                      ImageSource = new BitmapImage(new 
                          Uri(@"pack://application:,,,/MetronomeLibrary;component/Images/pad-metronome-decrement-button-over.png")) 
                  };
                  ButtonDecrement.Background = theBrush;
              }
      
              private void ButtonDecrement_MouseLeave(object sender, MouseEventArgs e)
              {
                  ImageBrush theBrush = new ImageBrush() 
                  { 
                      ImageSource = new BitmapImage(new 
                          Uri(@"pack://application:,,,/MetronomeLibrary;component/Images/pad-metronome-decrement-button.png")) 
                  };
                  ButtonDecrement.Background = theBrush;
              }
      
              private void SweepComplete(object sender, EventArgs e)
              {
                  BeatTimer.Stop();
                  BeatTimer.Start();
              }
      
              private void SetUpAnimation()
              {
                  NameScope.SetNameScope(this, new NameScope());
                  RegisterName(Arm.Name, Arm);
      
                  DoubleAnimation animationRotation = new DoubleAnimation()
                  {
                      From = -17,
                      To = 17,
                      Duration = new Duration(TimeSpan.FromMilliseconds(NumericDisplay.Milliseconds)),
                      RepeatBehavior = RepeatBehavior.Forever,
                      AccelerationRatio = 0.3,
                      DecelerationRatio = 0.3,
                      AutoReverse = true,                 
                  };
      
                  Timeline.SetDesiredFrameRate(animationRotation, 90);
      
                  MetronomeFlash.Opacity = 0;
      
                  DoubleAnimation opacityAnimation = new DoubleAnimation()
                  {
                      From = 1.0,
                      To = 0.0,
                      AccelerationRatio = 1,
                      BeginTime = TimeSpan.FromMilliseconds(NumericDisplay.Milliseconds - 0.5),
                      Duration = new Duration(TimeSpan.FromMilliseconds(100)),
                  };
      
                  Timeline.SetDesiredFrameRate(opacityAnimation, 10);
      
                  storyboard.Duration = new Duration(TimeSpan.FromMilliseconds(NumericDisplay.Milliseconds * 2));
                  storyboard.RepeatBehavior = RepeatBehavior.Forever;
                  Storyboard.SetTarget(animationRotation, Arm);
                  Storyboard.SetTargetProperty(animationRotation, new PropertyPath("(UIElement.RenderTransform).(RotateTransform.Angle)"));
                  Storyboard.SetTarget(opacityAnimation, MetronomeFlash);
                  Storyboard.SetTargetProperty(opacityAnimation, new PropertyPath("Opacity"));    
                  storyboard.Children.Add(animationRotation);
                  storyboard.Children.Add(opacityAnimation);
      
                  Resources.Add("Storyboard", storyboard);    
              }
          }
      }
      

4 个答案:

答案 0 :(得分:2)

使用WPF动画可能无法轻松实现。相反,一个好的方法是game loop。一点点的研究应该为此提供大量资源。第一个跳出来的是http://www.nuclex.org/articles/3-basics/5-how-a-game-loop-works

在游戏循环中,您将遵循以下一个或另一个基本程序:

  • 计算自上一帧以来经过的时间。
  • 适当移动您的显示器。

  • 计算当前时间。
  • 正确放置显示器。

游戏循环的优势在于虽然时间可能略有偏差(取决于您使用的时间类型),但所有显示器都会漂移相同的数量。

您可以通过系统时钟计算时间来防止时钟漂移,实际上这不会漂移。定时器确实漂移,因为它们不是由系统时钟运行。

答案 1 :(得分:1)

时间同步是一个比你想象的更广阔的领域。

我建议你看一下以调度/计时器问题而闻名的Quartz.NET

同步WPF动画很棘手,因为故事板不是逻辑树的一部分,因此您无法绑定其中的任何内容。
这就是为什么你不能在XAML中定义动态/可变的故事板,你必须像在C#中那样去做。

我建议你制作两个故事板:一个用于左边的刻度,另一个用于右边 在每个动画之间,触发一个方法来进行计算/更新UI的另一部分,但是在单独的Task中进行,以便时间不会混乱(计算的几μs弥补)已经30多岁了很长一段时间!)
请注意,您需要使用Application.Current.Dispatcher中的Task更新用户界面。

最后,至少设置Task标志TaskCreationOptions.PreferFairness,以便任务按照启动顺序运行。
现在,由于这只是给TaskScheduler提示并且不保证它们按顺序运行,您可能需要使用排队系统来代替完全保证。

HTH,

巴布。

答案 2 :(得分:0)

您可以尝试2个动画,一个用于右侧摆动,另一个用于左侧。在每个动画完成时,启动另一个动画(检查取消标记)并更新指示符(可能通过Dispatcher上的BeginInvoke,这样就不会干扰下一个动画启动。)

答案 3 :(得分:0)

我认为让计时器与动画同步很困难 - 它是一个基于调度程序的计时器,它基于消息 - 有时它可以跳过一点时间,即如果你用鼠标快速点击很多我认为动画计时器也是基于调度程序的,因此它们很容易失去同步。

我建议放弃同步并让计时器处理它。难道你不能通过通知更新属性并让你的节拍器手臂位置绑定到那个? 要获得加速/减速,您只需使用正弦或余弦函数。