【问题标题】:How can I get rid of jerkiness in WinForms scrolling animation?如何摆脱 WinForms 滚动动画中的抖动?
【发布时间】:2009-06-04 12:39:17
【问题描述】:

我正在用 C# 编写一个像图片框一样工作的简单控件,除了图像不断向上滚动(并从底部重新出现)。动画效果由计时器 (System.Threading.Timer) 驱动,该计时器从缓存图像(分为两部分)复制到隐藏缓冲区,然后在其 Paint 事件中将其绘制到控件的表面。

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

有什么办法可以让这个动画流畅滚动?

您可以下载一个示例应用程序here(运行它并单击“开始”),源代码为here。它看起来不是很生涩,但如果你仔细观察,你会发现打嗝。

警告:此动画会产生一种非常奇怪的视错觉效果,可能会让您感到有些不适。如果您观看一段时间然后将其关闭,它会看起来好像您的屏幕正在垂直拉伸。

更新:作为一个实验,我尝试用我的滚动位图创建一个 AVI 文件。结果没有我的 WinForms 动画那么生涩,但仍然无法接受(而且观看时间过长仍然让我感到恶心)。我认为我遇到了与刷新率不同步的根本问题,所以我可能不得不坚持让人们对我的外表和个性感到厌恶。

【问题讨论】:

  • 您好,您是否设法解决了 GDI 的问题,或者您是否转而使用 WPF?您能否告知您是如何进行的,以便其他面临同样问题的人可以从您的经验中受益。

标签: c# graphics animation


【解决方案1】:

在绘制缓冲图像之前,您需要等待 VSYNC

有一个CodeProject article 建议使用多媒体计时器和DirectX 方法IDirectDraw::GetScanLine()

我很确定您可以通过 C# 中的 Managed DirectX 使用该方法。

编辑:

经过更多研究和谷歌搜索后,我得出的结论是,通过 GDI 绘图不会实时发生,即使您在正确的时间绘图,它实际上也可能发生得太晚,并且您会流泪。

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

【讨论】:

  • 是什么让您得出结论 GDI 不会实时发生,但 DirectX 会发生?
  • 我偶然发现了这篇文章:virtualdub.org/blog/pivot/entry.php?id=74 - 因为使用 DirectX 或 DirectDraw 实际上可以实现平滑滚动而不会撕裂,我认为它“更实时”;)
  • .. 由于在 GDI 中绘制是通过消息进行的,因此消息总是有可能排队。
  • 感谢您的链接。我已经确信这是一个本质上不可能的问题(我讨厌承认任何事情),那篇文章证实了这一点。到目前为止,您是获得赏金的最佳人选。
【解决方案2】:

(http://www.vcskicks.com/animated-windows-form.html)

这个链接有一个动画,他们解释了他们完成它的方式。还有一个示例项目,您可以下载以查看它的实际效果。

【讨论】:

    【解决方案3】:

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

    另一个需要考虑的因素是,使用计时器并不能保证您在正确的时间被呼叫。执行此操作的正确方法是查看自上次抽签以来经过的时间并计算正确的距离以顺利移动。

    【讨论】:

    • 它已经被双缓冲了。问题没有闪烁。
    • 然后考虑我回答的第二部分:您的计时器事件没有在您期望的确切时间被调用。
    • 是的,由于 Windows 多任务处理,我的时间被量化为 15 毫秒,我正在尝试为持续 20 毫秒的帧设置动画。我认为我的问题无法解决。
    • 这只是意味着您必须偶尔跳过一帧或将内容移动非整数距离。查看基本游戏文本以了解其计算方式。
    • 不能跳帧或非整数距离移动。滚动的平滑度正是我想要的。根据我所读到的内容,我必须将动画与显示器的刷新率同步,而我根本无法做到这一点,因为我还必须将动画与音乐同步。
    【解决方案4】:

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

    You might want to give it a try.

    【讨论】:

    • 不是我。我投票给你,即使我不能使用 WPF。有人可能认为您的评论并不能真正回答我的问题,因为 WinForms 在技术上不是 WPF。我应该只说“Windows 应用程序”而不是 WinForms。
    • 你是对的。我没有注意到使用 WinForms 的重要性。那我再看看你的代码。
    【解决方案5】:

    您需要停止依赖定时器事件在您要求时准确触发,而是计算时间差,然后计算移动距离。游戏和WPF就是这样做的,这也是它们可以实现平滑滚动的原因。

    假设您知道您需要在 1 秒内移动 100 个像素(与音乐同步),然后您计算自上次触发计时器事件以来的时间(假设是 20 毫秒)并计算出移动距离作为总数的一小部分(20 ms / 1000 ms * 100 像素 = 2 像素)。

    粗略的示例代码(未测试):

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

    【讨论】:

      【解决方案6】:

      一些想法(并非都是好的!):

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

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

      • 如上所述,使用帧之间的时间来计算滚动的距离,这样滚动速度与帧率无关。但请注意,如果您尝试以非像素速率滚动(例如,如果您需要在一帧中滚动 1.4 像素,您可以做的最好的事情是 1 像素,这将给出 40 % 速度误差)。解决此问题的方法是使用较大的离屏位图进行滚动,然后在移动到屏幕时将其缩小,这样您就可以有效地按子像素量滚动。

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

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

      【讨论】:

      • 我实际上是从音频输出引擎的回调中驱动这个动画,所以理论上我的时间精确到 1/44 毫秒。我已经尝试过非全像素运动,但尤其是这些图像,插值伪影很严重,而且它仍然看起来很生涩。我已经尝试过更高的线程优先级,但它没有我可以检测到的效果。我想我可能不得不走 DirectX 路线,尽管我希望避免这种情况(我认为它在 Windows Mobile 中不起作用,这是一个问题)。
      【解决方案7】:

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

      【讨论】:

      • 可能不会。我用的是东芝Satellite笔记本,只能做到60赫兹。
      • 您使用的是哪种型号的笔记本?
      • 它是卫星 A135,功率相对不足(1.5G 内存,1.60GHz 处理器)。如果您有更好的视频卡,示例应用中的动画对您来说是否流畅?
      • Acer Veriton L460 和我的桌面——运行非常流畅。
      • 宏碁使用液晶显示器,台式机使用平板(都是戴尔的)。
      【解决方案8】:

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

      【讨论】:

      • 您的刷新率是否高于 60 Hz?发布此问题后,我尝试使用多媒体计时器,但没有看到任何明显的改进。
      • 不,我以 60Hz 的频率运行(在 TFT 显示器上)。很难用平易近人的术语来描述抽搐,但改进是“抽搐”的“频率”显着下降,即每隔 10-12 秒而不是每 1-2 秒偶尔出现一次向上的“跳跃”。不过,撕裂仍然让人分心,所以我并不是说 MM 计时器是 100% 的解决方案,但它确实提供了计时精度。
      【解决方案9】:

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

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

      其实你甚至可以在控件中添加一个属性来调整步长。

      【讨论】:

      • 当我放慢计时器但一次移动超过 1 个像素时,效果更差。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-05-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多