C# 如何消除WinForms滚动动画中的抖动?

C# 如何消除WinForms滚动动画中的抖动?,c#,graphics,animation,C#,Graphics,Animation,我正在用C#编写一个简单的控件,它的工作原理类似于图片框,只是图像不断向上滚动(并从底部重新显示)。动画效果由计时器(System.Threading.timer)驱动,该计时器将缓存图像(分为两部分)复制到隐藏缓冲区,然后在绘制事件中将其绘制到控件的曲面 问题是,当以每秒20多帧的高帧速率运行时(在较低帧速率下,效果太小而无法感知),此滚动动画效果会稍微抖动。我怀疑这种抖动是因为动画没有以任何方式与显示器的刷新率同步,这意味着每个帧在屏幕上停留的时间长度可变,而不是正好25毫秒 有没有办法让这

我正在用C#编写一个简单的控件,它的工作原理类似于图片框,只是图像不断向上滚动(并从底部重新显示)。动画效果由计时器(System.Threading.timer)驱动,该计时器将缓存图像(分为两部分)复制到隐藏缓冲区,然后在绘制事件中将其绘制到控件的曲面

问题是,当以每秒20多帧的高帧速率运行时(在较低帧速率下,效果太小而无法感知),此滚动动画效果会稍微抖动。我怀疑这种抖动是因为动画没有以任何方式与显示器的刷新率同步,这意味着每个帧在屏幕上停留的时间长度可变,而不是正好25毫秒

有没有办法让这个动画平滑地滚动

您可以下载一个示例应用程序(运行它并单击“开始”),源代码如下所示。它看起来并没有可怕的抽搐,但如果你仔细观察,你可以看到打嗝

警告:此动画产生一种非常奇怪的视错觉效果,可能会让你有点恶心。如果你看一段时间,然后关掉它,它会看起来就像你的屏幕在垂直伸展


更新:作为一个实验,我尝试用滚动位图创建一个AVI文件。结果没有我的WinForms动画那么急促,但仍然不可接受(而且看了太久还是让我恶心)。我想我遇到了一个根本性的问题,就是无法与刷新率同步,因此我可能不得不坚持让人们对我的外表和个性感到恶心。

使用双缓冲。这里有两篇文章:


另一个需要考虑的因素是,使用计时器并不能保证在正确的时间给你打电话。正确的方法是查看自上次绘制以来经过的时间,并计算平滑移动的正确距离

如果您想以较低的速度运行计时器,您可以随时更改视图中滚动图像的数量。这提供了更好的性能,但使效果看起来有点不稳定

只需更改_T+=1;要添加新步骤的行


实际上,您甚至可以向控件添加一个属性来调整步长amount。

几个月前,我遇到了一个类似的问题,通过切换到WPF解决了这个问题。动画控件比标准的基于计时器的解决方案运行得更流畅,我不再需要处理同步问题

一些想法(不全是好的!):

  • 使用线程计时器时,请检查渲染时间是否明显小于一帧间隔(从程序的声音来看,应该可以)。如果渲染时间超过1帧,则会收到重入调用,并在完成最后一帧之前开始渲染新帧。一种解决方案是在启动时只注册一个回调。然后在回调中,设置一个新的回调(而不仅仅是要求每隔n毫秒重复调用一次)。这样,您可以保证仅在渲染完当前帧后才计划新帧

  • 不要使用线程计时器,线程计时器会在不确定的时间量(唯一的保证是它大于或等于指定的间隔)后回调,而是在单独的线程上运行动画,然后简单地等待(busy wait loop或spinwait),直到下一帧的时间。您可以使用Thread.Sleep来缩短睡眠时间,以避免使用100%的CPU或Thread.Sleep(0)只是为了尽快获得另一个时间片。这将帮助您获得更加一致的帧间隔

  • 如上所述,使用帧间时间计算滚动距离,以便滚动速度与帧速率无关。但请注意,如果您尝试按非像素速率滚动,则会获得时间采样/混叠效果(例如,如果您需要在一帧中滚动1.4像素,则最好只滚动1像素,这将产生40%的速度错误)。一个解决方法是使用一个较大的屏幕外位图进行滚动,然后在切换到屏幕时将其缩小,这样可以有效地按亚像素量滚动

  • 使用更高的线程优先级。(非常讨厌,但可能会有帮助!)

  • 使用更可控的东西(DirectX)而不是GDI进行渲染。这可以设置为交换vsync上的屏幕外缓冲区。(我不确定表单的双缓冲是否会影响同步)


在绘制缓冲图像之前,需要等待一段时间

有人建议使用多媒体定时器和DirectX方法

我很确定你可以通过C#使用这种方法

编辑:

经过更多的研究和谷歌搜索,我得出结论,通过GDI绘图不会实时发生,即使你在正确的时刻绘图,也可能发生得太晚,你会流泪


因此,对于GDI来说,这似乎是不可能的。

我以前也遇到过同样的问题,发现这是一个视频卡问题。
你确定你的显卡可以处理吗?

我修改了你的例子,使用了一个多媒体定时器,精度可以降到1毫秒,大部分的抖动都消失了。但是,仍然有一些小撕裂留下,这取决于您垂直拖动窗口的确切位置。如果您想要一个完整完美的解决方案,GDI/GDI+可能不是您的方式,因为(AFAIK)它无法控制垂直同步。

您需要停止完全依赖计时器事件触发,而是计算出时差,然后计算出移动的距离。这就是游戏和WPF所做的,这就是为什么它们可以实现平滑的CRO
Image image = Image.LoadFromFile(...);
DateTime lastEvent = DateTime.Now;
float x = 0, y = 0;
float dy = -100f; // distance to move per second

void Update(TimeSpan elapsed) {
    y += (elapsed.TotalMilliseconds * dy / 1000f);
    if (y <= -image.Height) y += image.Height;
}

void OnTimer(object sender, EventArgs e) {
    TimeSpan elapsed = DateTime.Now.Subtract(lastEvent);
    lastEvent = DateTime.Now;

    Update(elapsed);
    this.Refresh();
}

void OnPaint(object sender, PaintEventArgs e) {
    e.Graphics.DrawImage(image, x, y);
    e.Graphics.DrawImage(image, x, y + image.Height);
}