Drawing an image onto a Panel control gives artefacts when resizing
Asked Answered
S

2

7

Currently I'm trying to do what I thought would be a simple task:

Draw an image onto the full area of a Panel control in Windows Forms. (Please ignore for the moment that I could use the BackgroundImage property)

The image to draw looks like this:

enter image description here

I.e. a yellow box with an 1 pixel blue frame around.

To draw, I'm using the Paint event of the Panel control:

private void panel1_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.DrawImage(Resources.MyImage, panel1.ClientRectangle);
}

This looks fine when initially displaying the form:

enter image description here

When resizing the form (and the docked panel, too), it either cuts the edges when being made smaller...

enter image description here

...or it draws artefacts, when being made larger:

enter image description here

I'm pretty sure that there is going on something rather simple and straight-forward but I really cannot understand the reason.

Since I'm ignoring the ClipRectangle and always draw everything, I thought the image would be scaled all the time.

My questions are:

  • What is the reason for the artefacts? (I love to understand this!)
  • What do I have to do in order to get rid of the artefacts? (beside calling Invalidate on each resize)

Update, SOLUTION:

Thanks to Ryan's answer, I was able to find an acceptable solution. Basically I derived a class from Panel, did an override of OnPaintBackground and did not call the base method. Last, I added the following code to the constructor of my derived panel:

base.DoubleBuffered = true;

SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.ResizeRedraw, true);
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

UpdateStyles();
Stalinist answered 17/3, 2012 at 15:49 Comment(1)
possible duplicate of #1970696Hairbrush
W
5

The reason for the artefacts is that the entire surface isn't redrawn when the form is resized; only the necessary parts are. The generally best solution is what you don't want to do, calling Invalidate on each resize. However, if this is in fact your situation, just use a PictureBox instead. If it's not, you might consider overriding OnPaint in your form instead, and using this.SetStyle(ControlStyles.ResizeRedraw, true) to do this automatically.

Washin answered 17/3, 2012 at 15:54 Comment(2)
Any chance to reduce the flickering when calling Invalidate? (I do think this comes from the background being drawn first) Maybe turning of background drawing/filling?Stalinist
@UweKeim: You'll need to enable double-buffering; subclass Panel, use OnPaint instead of handling Paint, and set the DoubleBuffered property to true.Washin
L
1

I used the OnResize, Invalidate approach but with a debouncer class:

internal class ProjectListControl : ScrollableControl {

    private readonly Debouncer? myResizeDebouncer;
    
    public ProjectListControl() {       
        myResizeDebouncer = new Debouncer(TimeSpan.FromSeconds(0.2), () => {
            BeginInvoke(Invalidate);
        });
    }
    
    // ... blah
    
    protected override void OnResize(EventArgs e) {
        base.OnResize(e);
        myResizeDebouncer?.Invoke();
    }
    
    protected override void OnPaint(PaintEventArgs e) {
        base.OnPaint(e);
        // etc.
    }
}

And the debouncer class was filtched from some other S.O. post (I forget where):

public class Debouncer : IDisposable {

    readonly TimeSpan myTimespan;
    readonly Action myAction;
    readonly HashSet<ManualResetEvent> myResets = [];
    readonly object myMutex = new();

    public Debouncer(TimeSpan timespan, Action action) {
        myTimespan = timespan;
        myAction = action;
    }

    public void Invoke() {
        var thisReset = new ManualResetEvent(false);

        lock (myMutex) {
            while (myResets.Count > 0) {
                var otherReset = myResets.First();
                myResets.Remove(otherReset);
                otherReset.Set();
            }

            myResets.Add(thisReset);
        }

        ThreadPool.QueueUserWorkItem(_ => {
            try {
                if (!thisReset.WaitOne(myTimespan)) {
                    myAction();
                }
            }
            finally {
                lock (myMutex) {
                    using (thisReset)
                        myResets.Remove(thisReset);
                }
            }
        });
    }

    public void Dispose() {
        lock (myMutex) {
            while (myResets.Count > 0) {
                var reset = myResets.First();
                myResets.Remove(reset);
                reset.Set();
            }
        }
    }
}
Londalondon answered 7/6 at 9:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.