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

时间:2009-06-04 12:39:17

标签: c# graphics animation

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

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

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

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

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

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

9 个答案:

答案 0 :(得分:3)

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

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

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

修改

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

所以,对于GDI来说,这似乎是不可能的。

答案 1 :(得分:3)

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

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

答案 2 :(得分:2)

使用双缓冲。以下是两篇文章:1 2

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

答案 3 :(得分:2)

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

You might want to give it a try.

答案 4 :(得分:2)

你需要在你提出要求时停止依赖定时器事件的准确触发,然后计算时差,然后计算移动的距离。这就是游戏和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);
}

答案 5 :(得分:1)

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

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

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

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

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

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

答案 6 :(得分:1)

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

答案 7 :(得分:1)

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

答案 8 :(得分:0)

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

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

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