如何摆脱WinForms滚动动画中的混蛋?

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

问题在于,当以每秒20+帧的高帧速率运行时,这种滚动动画效果略微不稳定(在较低的帧速率下,效果太小而无法被感知)。 我怀疑这种混蛋是因为动画与我的显示器的刷新率没有任何同步,这意味着每个帧在屏幕上保持可变的时间长度而不是恰好25毫秒。

有什么方法可以让这个动画顺利滚动吗?

您可以在此处下载示例应用程序(运行它并单击“开始”),源代码在此处 。 它看起来并不可怕,但是如果你仔细观察它就可以看到打嗝。

警告 :这个动画产生一种非常奇怪的视错觉效果,可能会让你有点不舒服。 如果您观看一段时间然后将其关闭,它看起来就像您的屏幕垂直伸展一样。

更新 :作为实验,我尝试用我的滚动位图创建一个AVI文件。 结果不如我的WinForms动画生涩,但仍然是不可接受的(它仍然让我感到厌倦了我的胃,看了太久)。 我想我遇到了一个根本没有与刷新率同步的问题,所以我可能不得不坚持让人们因为我的外表和个性而生病。

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

有一篇CodeProject文章建议使用多媒体计时器和DirectX’方法IDirectDraw::GetScanLine()

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

编辑:

经过一些研究和谷歌搜索,我得出的结论是,通过GDI绘图并不是实时发生的,即使你正在绘制一个恰当的时刻,它实际上可能发生得太晚而且你会撕裂。

所以,有了GDI,这似乎是不可能的。

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

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

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

另一个值得思考的因素是使用计时器并不能保证在恰当的时间被调用。 这样做的正确方法是查看自上次绘制以来经过的时间并计算正确移动的正确距离。

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

你可能想尝试一下。

当你要求时,你需要停止依赖定时器事件的准确触发,然后计算出时差,然后计算移动的距离。 这就是游戏和WPF所做的,这就是他们可以实现平滑滚动的原因。

假设你知道你需要在1秒钟内移动100个像素(与音乐同步),然后你计算自上一个计时器事件被触发以来的时间(假设它是20ms)并计算距离以便作为一个分数移动总数(20毫秒/ 1000毫秒* 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); } 

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

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

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

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

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

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

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

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

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

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

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