【问题标题】:C# moving Mouse interrupst Dispatcher TimerC# 移动鼠标中断调度程序定时器
【发布时间】:2021-07-06 06:19:12
【问题描述】:

我目前正在开发一个程序,我想使用 Dispatcher Timer 自动移动一个椭圆。只要我的鼠标静止不动,它就可以正常工作。但是,当我开始移动鼠标时,椭圆移动得非常慢,甚至完全停止移动。当停止移动它时,程序再次正常运行。椭圆是 Canvas 的 Child,背景中除此 Canvas 外没有其他 UI 元素。我也没有在画布上添加任何事件。 我的目标是,将鼠标移到应用程序上,而不影响动画。 有人对解决这个问题有什么建议吗? 先感谢您! 蔚蓝

这是一个最小可重现示例的所有代码: Xaml.cs:

public partial class MainWindow : Window
{
    public DispatcherTimer timer = new DispatcherTimer();
    public MainWindow()
    {
        InitializeComponent();

        Canvas.SetLeft(Ball, 0);
        Canvas.SetTop(Ball, 225);

        timer.Interval = TimeSpan.FromMilliseconds(4.16666666);
        timer.Tick += Move;

        timer.Start();
    }

    private void Move(object sender, EventArgs e)
    {
        if ((MapCanvas.ActualWidth - Ball.Width) > Canvas.GetLeft(Ball))
        {
            Canvas.SetLeft(Ball, Canvas.GetLeft(Ball) + 2);
        }
    }
}

这里是 xaml:

<Window x:Class="BallSpielExample.MainWindow"
    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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:BallSpielExample"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">
<Grid>
    <Canvas x:Name="MapCanvas" Background="NavajoWhite" Width="auto" Height="auto">
        <Ellipse Name="Ball" Width="30" Height="30" Fill="SaddleBrown"/>
    </Canvas>
</Grid>

【问题讨论】:

  • 您最好创建一个minimal reproducible example 并将其包含在您的问题中,这样我们就不必猜测您的虚构代码存在哪些问题。
  • 好的,谢谢,我正在努力。
  • DispatcherTimer 与其他 Dispatcher 操作在同一线程上执行。检查设置不同的DispatcherPriorityDispatcherTimer 的构造函数之一的参数)是否会有所帮助。如果没有,请考虑改用System.Timers.Timer - 它在单独的线程上运行(您必须使用InvokeBeginInvoke 从其Elapsed 事件更新GUI)。 docs.microsoft.com/en-us/dotnet/api/…
  • 我认为您没有充分利用 wpf 中的动画。尝试双动画
  • 绝对可以使用 DispatcherTimer 而不是 CompositionTarget.Rendering 或一些动画。您只需要确保时间间隔足够大,例如30-50 毫秒,并且您以与接受的答案中所示类似的方式确定自上次 Tick 处理程序调用以来经过的时间。可选地通过 DateTime.Now 而不是秒表。

标签: c# wpf animation timer


【解决方案1】:

第一个问题是 DispatcherTime 不够稳定,所以对动画没有用处。第二个问题是您应该使用(增量)时间来计算运动。如果您使用 WPF 动画,您将失去对其位置等的控制(边界检查等)

我会为此使用CompositionTarget.Rendering 事件。这样你就可以在渲染线程上移动球了。

使用秒表来计时,因为您希望它在不同的设备上以相同的速度移动。

我做了一个小例子来展示它是如何工作的:

MainWindow.xaml

<Window x:Class="BouncingBall.MainWindow"
            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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:BouncingBall"
            mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800">
    <Canvas x:Name="MapCanvas">
        <Ellipse Name="Ball" Width="30" Height="30" Fill="SaddleBrown"/>
    </Canvas>
</Window>

MainWindow.xaml.cs

/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    private double _ballPositionX = 100;
    private double _ballPositionY = 100;
    private double _ballSpeedX = 225; // pixels per second
    private double _ballSpeedY = 150; // pixels per second

    private TimeSpan _previousTime;

    private readonly Stopwatch _stopwatch = Stopwatch.StartNew();

    public MainWindow()
    {
        InitializeComponent();

        _previousTime = _stopwatch.Elapsed;

        // set the (initial) position
        UpdateBall(0);

        // Attach the Update method on the WPF renderTarget <----
        CompositionTarget.Rendering += Update;
    }

    private void Update(object sender, EventArgs e)
    {
        // check delta time
        var currentTime = _stopwatch.Elapsed;
        var delta = (currentTime - _previousTime).TotalSeconds;
        _previousTime = currentTime;

        UpdateBall(delta);
        // if you want to animate multiple shapes, Call something like UpdateBall
    }

    private void UpdateBall(double delta)
    {
        // ensure that the ball moves by using the deltatime.
        _ballPositionX += _ballSpeedX * delta;
        _ballPositionY += _ballSpeedY * delta;

        // check bounds X
        if (_ballPositionX < 0)
        {
            _ballSpeedX = -_ballSpeedX;
            _ballPositionX = 0;
        }
        else if (_ballPositionX > MapCanvas.ActualWidth - Ball.ActualWidth)
        {
            _ballSpeedX = -_ballSpeedX;
            _ballPositionX = MapCanvas.ActualWidth - Ball.ActualWidth;
        }

        // check bounds Y
        if (_ballPositionY < 0)
        {
            _ballSpeedY = -_ballSpeedY;
            _ballPositionY = 0;
        }
        else if (_ballPositionY > MapCanvas.ActualHeight - Ball.ActualHeight)
        {
            _ballSpeedY = -_ballSpeedY;
            _ballPositionY = MapCanvas.ActualHeight - Ball.ActualHeight;
        }

        // update the position
        Canvas.SetLeft(Ball, _ballPositionX);
        Canvas.SetTop(Ball, _ballPositionY);
    }
}

【讨论】:

  • 您好,非常感谢您提供这样的质量。我知道 deltatime 问题。但我不明白是什么原因导致“更新”方法频繁运行?它多久运行一次?
  • @Azuro 更新方法附加到CompositionTarget.Rendering 事件处理程序(参见 MainWindow 构造函数的最后一行代码)。触发大约 60 fps(它是 WPF 渲染线程)。
  • 好的,非常感谢。有什么方法可以增加或减少每秒的触发器数量吗?
  • 不,你为什么想要那个?在无法绘制它们的情况下计算更快的点是没有用的。如果你想要更多的 fps,你不应该使用 WPF
  • WPF 未针对渲染许多(移动的)精灵进行优化。下一步是类似于direct-x。但要困难得多。
【解决方案2】:

使用下面的动画

private void Move(object sender, EventArgs e)
{
    if ((MapCanvas.ActualWidth - Ball.Width) >= Canvas.GetLeft(Ball))
    {
        MoveAnim(Ball, .00001, Canvas.GetLeft(Ball), Canvas.GetLeft(Ball) + 2);
    }
}
public static void MoveAnim(UIElement element, double duration, double from, double to)
{
    string property = "(Canvas.Left)";
    Storyboard myStoryboard = new Storyboard();
    myStoryboard.FillBehavior = FillBehavior.HoldEnd;
    DoubleAnimation animation = new DoubleAnimation();

    animation.From = from;
    animation.To = to;
    animation.Duration = TimeSpan.FromSeconds(duration);
    animation.FillBehavior = FillBehavior.HoldEnd;
    Storyboard.SetTarget(animation, element);
    Storyboard.SetTargetProperty(animation, new PropertyPath(property));
    myStoryboard.Children.Add(animation);
    myStoryboard.Begin();
}

【讨论】:

    【解决方案3】:
    timer.Interval = TimeSpan.FromMilliseconds(4.16666666);
    

    没有多大意义。 DispatcherTimer 既不应该以 200 Hz 的频率运行,也不应该精确地运行到几分之一毫秒。 Ellipse 应该由 DoubleAnimation 或 PointAnimation 移动。

    以下示例中的 DoubleAnimation 每 5 毫秒将 Ellipse 的 Canvas.Left 属性动画化 2 个单位。由于动画永远运行,您也可以为 ByDuration 属性使用其他值,但保持它们的比率,例如By = 20Duration = TimeSpan.FromMilliseconds(50)

    public MainWindow()
    {
        InitializeComponent();
    
        Canvas.SetLeft(Ball, 0);
        Canvas.SetTop(Ball, 225);
    
        var animation = new DoubleAnimation
        {
            By = 2,
            Duration = TimeSpan.FromMilliseconds(5),
            IsCumulative = true,
            RepeatBehavior = RepeatBehavior.Forever
        };
    
        Ball.BeginAnimation(Canvas.LeftProperty, animation);
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-12-25
      • 2011-04-29
      • 2022-01-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多