7

下午好,

在过去的几周里,我一直在做一个创建高级节拍器的项目。节拍器由以下几部分组成

  1. 摆动的手臂
  2. 闪光
  3. 一组动态创建的代表节拍的用户控件(其中 4 个是打开、重音或关闭)。
  4. 一个用户控件,用于显示 LCD 数字显示并计算所选 BPM 的节拍之间的毫秒数(60000/BPM=毫秒)

用户选择一个 BPM 并按下开始,会发生以下情况

  1. 手臂以每次扫描 n 毫秒的速度在两个角度之间摆动
  2. 每次手臂扫描结束时灯会闪烁
  3. 创建指示器并按顺序闪烁(每次扫描结束时闪烁)。

现在的问题是 Arm 和 light flash 动画是在代码中创建的,并添加到故事板中,并具有永远重复和自动反转的功能。

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

所以,经过一番折腾,我所做的是创建一个与故事板运行速度相同的计时器。

问题是,超过 30 秒,计时器和情节提要不同步,因此指示器和手臂扫描不及时(不适合节拍器!!)。

我试图捕捉动画的完成事件并将其用作停止和重新启动计时器的触发器,这就是我能想出的让两者完美同步的方法。

不同步的原因是情节提要滑动以及情节提要在使用 .start 调用计时器之前在线上使用 begin 调用的事实,尽管我认为这微秒意味着它们开始不可能关闭但不完全相同时间。

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

谁能想到另一种(更狡猾的)方法来保持这两件事同步。

最后,我确实想看看我是否可以从情节提要中触发一个方法(这会让我的生活变得非常轻松,但看起来这似乎无法做到)。

如果有什么建议我不珍贵,我只想完成这个!

最后一点,可以在节拍器运行时调整 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

4 回答 4

2

这可能不容易用 WPF 动画实现。相反,一个好方法是游戏循环。一点点研究应该会发现很多关于这个的资源。第一个让我兴奋的是http://www.nuclex.org/articles/3-basics/5-how-a-game-loop-works

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

  • 计算自上一帧以来经过了多少时间。
  • 适当地移动你的显示器。

或者

  • 计算当前时间。
  • 适当地放置您的显示器。

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

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

于 2012-07-07T15:03:06.933 回答
1

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

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

同步 WPF 动画很棘手,因为情节提要不是逻辑树的一部分,因此您不能在其中绑定任何内容。
这就是为什么您不能在 XAML 中定义动态/可变故事板的原因,您必须像以前那样在 C# 中进行定义。

我建议你制作 2 个故事板:一个用于左侧的勾号,另一个用于右侧。
在每个动画之间,触发一个方法来进行计算/更新 UI 的另一部分,但要单独进行Task,以免时间混乱(计算的几微秒在 30 秒后弥补了相当长的一段时间已经!)
请记住,您将需要使用Application.Current.DispatcherfromTask来更新 UI。

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

高温下,

呸。

于 2012-07-06T13:31:14.847 回答
0

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

于 2012-07-02T18:24:17.880 回答
0

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

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

于 2012-07-06T13:14:30.730 回答