How can I get rid of jerkiness in WinForms scrolli

2019-04-10 04:57发布

问题:

I'm writing a simple control in C# that works like a picture box, except the image is constantly scrolling upwards (and re-appearing from the bottom). The animation effect is driven by a timer (System.Threading.Timer) which copies from the cached image (in two parts) to a hidden buffer, which is then drawn to the control's surface in its Paint event.

The problem is that this scrolling animation effect is slightly jerky when run at a high frame rate of 20+ frames per second (at lower frame rates the effect is too small to be perceived). I suspect that this jerkiness is because the animation is not synchronized in any way with my monitor's refresh rate, which means that each frame stays on the screen for a variable length of time instead of for exactly 25 milliseconds.

Is there any way I can get this animation to scroll smoothly?

You can download a sample application here (run it and click "start"), and the source code is here. It doesn't look horribly jerky, but if you look at it closely you can see the hiccups.

WARNING: this animation produces a pretty weird optical illusion effect which might make you a little sick. If you watch it for awhile and then turn it off, it will look as if your screen is stretching itself vertically.

UPDATE: as an experiment, I tried creating an AVI file with my scrolling bitmaps. The result was less jerky than my WinForms animation, but still unacceptable (and it still made me sick to my stomach to watch it for too long). I think I'm running into a fundamental problem of not being synced with the refresh rate, so I may have to stick to making people sick with my looks and personality.

回答1:

You would need to wait for a VSYNC before you draw the buffered image.

There is a CodeProject article that suggests using a multimedia timer and DirectX' method IDirectDraw::GetScanLine().

I'm quite sure you can use that method via Managed DirectX from C#.

EDIT:

After some more research and googling I come to the conclusion that drawing via GDI doesn't happen in realtime and even if you're drawing in the exact right moment it might actually happen too late and you will have tearing.

So, with GDI this seems not to be possible.



回答2:

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

This link has an animation and they explain the way they accomplish it. There is also a sample project that you can download to see it in action.



回答3:

Use double buffering. Here are two articles: 1 2.

Another factor to ponder is that using a timer doesn't guarantee you to be called at exactly the right time. The correct way to do this is to look at the time passed since the last draw and calculate the correct distance to move smoothly.



回答4:

I had a similar problem a couple of months ago, and solved them by switching to WPF. The animated control ran a lot smoother than with a standard timer-based solution and I didn't have to take care of synchronization any more.

You might want to give it a try.



回答5:

You need to stop relying on the timer event firing exactly when you ask it to, and work out the time difference instead, and then work out the distance to move. This is what games, and WPF do, which is why they can achieve smooth scrolling.

Let's say you know you need to move 100 pixels in 1 second (to sync with the music), then you calculate the time since your last timer event was fired (let's say it was 20ms) and work out the distance to move as a fraction of the total (20 ms / 1000 ms * 100 pixels = 2 pixels).

Rough sample code (not tested):

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:

Some ideas (not all good!):

  • When using a threading timer, check that your rendering time is considerably less than one frame interval (from the sound of your program, you should be fine). If rendering takes longer than 1 frame, you will get re-entrant calls and will start rendering a new frame before you've finished the last. One solution to this is to register for only a single callback at startup. Then in your callback, set up a new callback (rather than just asking to be called repeatedly every n milliseconds). That way you can guarantee that you only schedule a new frame when you've finished rendering the current one.

  • Instead of using a thread timer, which will call you back after an indeterminate amount of time (the only guarantee is that it is greater than or equal to the interval you specified), run your animation on a separate thread and simply wait (busy wait loop or spinwait) until it is time for the next frame. You can use Thread.Sleep to sleep for shorter periods to avoid using 100% CPU or Thread.Sleep(0) simply to yield and get another timeslice as soon as possible. This will help you to get much more consistent frame intervals.

  • As mentioned above, use the time between frames to calculate the distance to scroll, so that the scroll speed is independent of the frame rate. But note that you will get temporal sampling/aliasing effects if you try to scroll by a non-pixel rate (e.g. if you need to scroll by 1.4 pixels in a frame, the best you can do is 1 pixel, which will give a 40% speed error). A workaround for this would be to use a larger offscreen bitmap for scrolling and then scale it down when blitting to screen, so you can effectively scroll by sub-pixel amounts.

  • Use a higher thread priority. (really nasty, but may help!)

  • Use something a bit more controllable (DirectX) rather than GDI for rendering. This can be set up to swap the offscreen buffer on a vsync. (I'm not sure if Forms' double buffering bothers with syncing)



回答7:

I had the same problem before and found it to be a video card issue. Are you sure your video card can handle it?



回答8:

I modified your example to use a multimedia timer that has a precision down to 1 ms, and most of the jerkiness went away. However, there is still some little tearing left, depending on where exactly you drag the window vertically. If you want a complete and perfect solution, GDI/GDI+ is probably not your way, because (AFAIK) it gives you no control over vertical sync.



回答9:

Well, if you wanted to run the timer at a lower speed, you can always change the ammount the image is scrolled in the view. This gives better preformance, but makes the effect look kinda jerky.

Just change the _T += 1; line to add the new step...

Actually, you could even add a property to the control to adjust the step ammount.