Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WinForms Layered Controls with Background images cause tearing while scrolling

Tags:

c#

.net

winforms

I have a Form with the following properties:

  • Background Image
  • Scrollable Panel with a transparent background, and Dock = DockStyle.Fill
  • PictureBox with a large Width and Height which shows scroll bars

Now all controls are set to DoubleBuffered including the form itself. Everything works as expected except when scrolling the Panel for the PictureBox, the form background image scrolls with it repeating itself showing vertical and horizontal tearing although its static image that fits the form's size, and when you stop scrolling it shows properly. This only happens when dragging the scrollbars, if i click on any point in the scrollbars to move it, it shows properly.

As per my understanding Double Buffering should eliminate such cases, but even with double buffering its the same, maybe a little bit better but still its a huge problem when scrolling.

I tried to place all controls inside another panel instead of using form background image and place this panel on the form but it didn't make any difference.

like image 614
YazX Avatar asked Dec 08 '22 01:12

YazX


1 Answers

You are doing battle with a Windows system option, named "Show window content while dragging". It is turned on for all modern versions of Windows. Turning it off is not a realistic goal, since it is a system option it affects all windows of all apps. There is no back-door to selectively bypass this option.

With it enabled, the OS optimizes the scrolling of a window. It performs a fast bitblt to move the pixels in the video frame buffer and generates a paint message for only the part of the window that is revealed by the scroll. Like the bottom few rows of pixels when you scroll down. Underlying winapi call is ScrollWindowEx(). Intention is to provide an app with a more responsive UI, a lot less work has to be done to implement the scroll.

You can probably see where this is heading, ScrollWindowEx() also moves the pixels that were painted by the form's BackgroundImage. You can see that. Next thing you see is the side-effect of the optimized paint, it only redraws the part of the window that was revealed. So the moved background image pixels don't get redrawn. Looks like a "smearing" effect.

There is a simple workaround for that, just implement an event handler for the panel's Scroll event and call Invalidate(). So the entire panel gets redrawn again:

    private void panel1_Scroll(object sender, ScrollEventArgs e) {
        panel1.Invalidate();
    }

But now you'll notice the side-effect of the paint no longer being optimized. You still see the pixels getting moved, then overdrawn. How visible that is depends a great deal on how expensive the BackgroundImage is to draw. Usually never cheap because it doesn't have the optimal pixel format (32bppPArgb) and doesn't have the right size so needs to be rescaled to fit the window. The visual effect resembles the "pogo", rapid jittering on one edge of the panel.

Pretty unlikely you'll find that acceptable or want to do the work to optimize the BackgroundImage. Stopping ScrollWindowEx() from doing its job requires a pretty big weapon, you can pinvoke LockWindowUpdate(). Like this:

 using System.Runtime.InteropServices;
 ...
    private void panel1_Scroll(object sender, ScrollEventArgs e) {
        if (e.Type == ScrollEventType.First) {
            LockWindowUpdate(this.Handle);
        }
        else {
            LockWindowUpdate(IntPtr.Zero);
            panel1.Update();
            if (e.Type != ScrollEventType.Last) LockWindowUpdate(this.Handle);
        }
    }

    [DllImport("user32.dll", SetLastError = true)]
    private static extern bool LockWindowUpdate(IntPtr hWnd);

Works pretty well, the background image pixels are now rock-steady. Any other pixels, well, not so much. Another visual effect, lets call it a "wrinkle". Getting rid of that artifact can be done by putting the window into compositing mode. Which double-buffers the entire window surface, including the child controls:

    protected override CreateParams CreateParams {
        get {
            const int WS_EX_COMPOSITED = 0x02000000;
            var cp = base.CreateParams;
            cp.ExStyle |= WS_EX_COMPOSITED;
            return cp;
        }
    }

Only remaining artifact is the side-effect of this not being very cheap code. It probably doesn't look that smooth when you scroll. Which otherwise tells you why windows were designed to be opaque 28 years ago.

like image 156
Hans Passant Avatar answered Dec 22 '22 00:12

Hans Passant