我在C#中编写一个简单的控件,其工作方式类似于图片框,但图像不断向上滚动(并从底部重新显示)。动画效果由计时器(System.Threading.Timer)驱动,计时器从缓存的图像(分为两部分)复制到隐藏缓冲区,然后在其Paint事件中将其绘制到控件的表面。
问题在于,当以每秒20+帧的高帧速率运行时,这种滚动动画效果略微不稳定(在较低的帧速率下,效果太小而无法被感知)。我怀疑这种混蛋是因为动画与我的显示器的刷新率没有任何同步,这意味着每个帧在屏幕上保持可变的时间长度,而不是恰好25毫秒。
有什么方法可以让这个动画顺利滚动?
您可以下载示例应用程序here(运行它并单击“开始”),源代码为here。它看起来并不可怕,但如果你仔细观察它就可以看到打嗝。
警告:这个动画会产生一种非常奇怪的视错觉效果,可能会让你有点不舒服。如果您观看它一段时间然后将其关闭,它看起来就像您的屏幕垂直伸展一样。
更新:作为实验,我尝试使用滚动位图创建AVI文件。结果不如我的WinForms动画生涩,但仍然是不可接受的(它仍然让我感到恶心,看着它太长时间)。我认为我遇到了一个根本没有与刷新率同步的问题,所以我可能不得不坚持让人们因为我的外表和个性而生病。
答案 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)
答案 3 :(得分:2)
几个月前我遇到了类似的问题,并通过切换到WPF解决了这个问题。与基于标准计时器的解决方案相比,动画控件运行得更顺畅,我不再需要处理同步。
答案 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;行添加新步骤...
实际上,您甚至可以向控件添加属性以调整步长值。