2

Scenario

Having a Windows Forms Form-derived form that contains a Panel-derived control:

enter image description here

The form gets a black background color set:

public MyForm()
{
    InitializeComponent();

    base.BackColor = Color.Black;
}

And the panel control is configured to be double buffered and the like, as described here, here and here:

public MyPanel()
{
    base.DoubleBuffered = true;

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

    UpdateStyles();
}

The actual drawing is done inside these overrides in MyPanel:

protected override void OnPaintBackground(PaintEventArgs e)
{
    e.Graphics.Clear(Color.Black);
}

protected override void OnPaint(PaintEventArgs e)
{
    e.Graphics.Clear(Color.Black);
}

Wrong behaviour

Every now and then, when the form is initially shown, my owner drawn panel is shortly drawn as white, before my own drawing code is actually called:

enter image description here

After my drawing code is called, everything is drawn correctly and I never can reproduce the white background again:

enter image description here

Even resizing the window and panel does not make it flicker in white.

Enforcing the wrong behavior

I can enforce the initial white-drawn of my panel, if I put a sleep in the Shown event handler of my form:

private void MyForm_Shown(object sender, EventArgs e)
{
    Thread.Sleep(1000);
}

The panel is shown in white for 1000 ms just before my owner-drawn paint code is being called.

My question

How can I avoid the initial white displaying when having an owner drawn/custom drawn panel?

My goal is to have the control "know" its initial background color (black in my example) right from the start, not after it is initially shown.

Some thoughts

I've tried to play with all kind of things including the CreateParams property, but with no visible success.

My initial idea was to provide some initial background color through the WNDCLASSEX structure, but after digging through the Reference Source, I still have no clue whether this is possible and would help.

Whole code

Just to be safe, following is my whole code.

MyForm.cs:

public partial class MyForm : Form
{
    public MyForm()
    {
        InitializeComponent();

        base.BackColor = Color.Black;
    }

    private void MyForm_Shown(object sender, EventArgs e)
    {
        Thread.Sleep(1000);
    }
}

MyPanel.cs:

public class MyPanel : Panel
{
    public MyPanel()
    {
        base.DoubleBuffered = true;

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

        UpdateStyles();
    }

    protected override void OnPaintBackground(PaintEventArgs e)
    {
        e.Graphics.Clear(Color.Black);
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        e.Graphics.Clear(Color.Black);
    }
}
Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
  • 1
    the problem is with the form's background, not the panel's. the form will have a clip region where the panel goes, until it learns that the panel will all-WM-paint itself, thus not painting that black initially. this is done in windows forms framework code. could you apply the same styles to the panel's parent form? – Cee McSharpface Dec 02 '18 at 16:54
  • Thanks,@dlatikay, I've just tried that. Now [the _whole_ form is initially white](https://i.imgur.com/acSGUb2.png), before switching to black after the 1000 ms. Any chance to tell the form the initial color black? – Uwe Keim Dec 02 '18 at 16:58
  • 1
    The only way to reproduce this is to keep the parent Form message queue busy. Inserting `Thread.Sleep(1000);` in the `Shown` event will prevent the immediate painting of any control on the Form, leaving their clipping region white-ish for the time it sleeps. You can prevent it, for example, setting your Panel's visibility to `false` and re-setting to `true` as the last line in the `Shown` event. – Jimi Dec 02 '18 at 17:02
  • Thanks, @Jimi, is the white a hard-coded color value of WinForms or Windows itself? – Uwe Keim Dec 02 '18 at 17:05
  • 1
    I'm not really sure why it *looks* white-ish. Maybe it's just the result of the absence of a color in the internal container or the action of the VisualStyleRenderer. `Form.OnPaint` calls `base.OnPaint()`, where base is `ContainerControl`, which calls `ScrollableControl`, which calls `Control` and that's not a public object. Maybe Hans knows what's behind it. – Jimi Dec 02 '18 at 17:29
  • 1
    the [source code](https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/Control.cs,b27f0a18a7b66617,references) comment "if you do not want the default Windows behavior you must set `event.Handled` to true" is interesting, but obviously too late. another promising approach would be to follow the documentation for a 100% transparent form which avoids most of the background drawing, also the white-by-default if you're lucky (since transparency does not mean that you can't ownerdraw an opaque rectangle in onpaintbackground) – Cee McSharpface Dec 02 '18 at 17:34
  • 2
    If you set `AllPaintingInWmPaint` to `false` it will start black. I'm not sure whether that scuppers your double-buffering goals, but you could experiment with it, perhaps ignoring the WM_ERASEBKGND (except the first one) explicitly yourself in an overridden `WndProc()` instead. Otherwise I suspect registering a new class is the way to go, to override the default background colour which is otherwise "Control". As you point-out this is doable but quite a pain (https://stackoverflow.com/questions/128561/registering-a-custom-win32-window-class-from-c-sharp). – sellotape Dec 02 '18 at 17:45
  • 2
    @sellotape This could be a good idea. You could keep `this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);` in the Panel's constructor and disable the Form's `DoubleBuffering`. This should allow to paint both Form and Panel with the same color even when the message pump is busy. – Jimi Dec 02 '18 at 18:07
  • 1
    If a delay in the message pump processing a painting of the panel is the issue, possibly calling its Refresh method in the form's shown event would be sufficient. If not, a possible messy hack would be to hook the ParentChanged event to determine the containing form, set Bounds to zero and use FormShown to reset the Bounds. – TnTinMn Dec 03 '18 at 15:52
  • 1
    Maybe @HansPassant can give a hint on how to configure a core window (class) that its "default" color/brush is always black and not white? – Uwe Keim Jan 21 '19 at 11:56
  • With double buffer for me, was initially showing as white, and finally painting the background black and foreground like 4 frames later. However If use SetStyle(ControlStyles.OptimizedDoubleBuffer, true), and not set AllPaintingInWmPaint, and not use DoubleBuffered property (which also sets AllPaintingInWmPaint), fixed it. Otherwise, it flashes badly on start. Maybe with windows denied initial paint, can see .Net side painting very late, maybe as it waits to show the first buffered? And some default control color in the windows window class? – Beeeaaar Oct 09 '22 at 18:03
  • Ok all of your combined posts really helped me. All AllPaintingInWmPaint does is ignore the WM_ERASEBKGND message, which was the only thing that causes flicker for me to begin with and was reason unknowingly I turned on double buffer. So, what i did was add a DoubleBuffer property to my control, not DoubleBuffered, to allow users to change that only, not AllPaintingInWmPaint, and added my own "smart" WM_ERASEBKGND handling when my own DoubleBuffer flag is set. So the property sets SetStyle(ControlStyles.OptimizedDoubleBuffer and sets a flag to let me pick when I want erase and not. – Beeeaaar Oct 09 '22 at 18:37

1 Answers1

1

I had a custom control, pretty much completely self-drawn, that was dynamically getting added to a Form in the dropdown mechanism in PropertyGrid.

If I read your problem right, it's basically the same overall issue.

I had BackColor set and was fine. But with DoubleBuffer set, it seems to just ignore it for a bit.

Taking in all of the existing comments on the question, I was able to have this solution, and hope the details help someone else make their own.

Problem 1

My control flickered unless I repainted whole control every OnPaint.

If did proper painting only attempting to paint things that intersected the e.ClipRectangle, then it would flicker in real-time, like effects from invalidates that had to do when the mouse was moved. Attempt to Paint whole thing, no problem.

I watched trace output real-time and watched all of my draws and invalidates print, and never a time where should be introducing flicker myself, on myself directly.

Problem 2

If I turn on DoubleBuffered, then instead it flickered badly as the control was shown every time opening the dropdown. From white background only for 100-200 ms, at least, then to black and rendered foreground suddenly in one step.

Problem 3

I never actually needed double buffer. Both the problem 1 and 2 were always related to WM_ERASEBKGND.

The actual original flicker seems to be caused by WM_ERASEBKGND very briefly visibly whacking my already painted thing, right before I painted it again. Did not really need actual double buffering in my case. For some reason when I was blindly painting the whole list maybe the timing was different and was painting over the erase before could see it.

All that said, if I turn DoubleBuffered on which removes WM_ERASEBKGND via turning on AllPaintingInWmPaint, then initial background won't be painted until I suppose the double buffer and paint process works its way, all the way through the first time.

Problem 4

If I let the WM_ERASEBKGND "just happen", then it's still double painting, and I don't know if or when it might end up flicking anyway for someone else.

If I only turn on SetStyle(OptimizedDoubleBuffer, then I now know I'll be letting the initial background paint and not flicker on show. But I also know I'm using double buffer to mask the WM_ERASEBKGND for the entirety of the life of the control after it is shown.

So....

I did something like this:

Part 1

if the user of the control sees a need to double buffer doing something that might flicker, create a way for them to easily enable it, without forcing AllPaintingInWmPaint. Like if they want to use Paint or a DrawXXX event and doing something that animates or something related to mouse movement.

    bool _isDoubleBuffer;
    [Category("Behavior")]
    public virtual bool DoubleBuffer
    {
        get { return _isDoubleBuffer; } // dont care about real SetStyle state
        set
        {
            if (value != DoubleBuffer)
            {
                _isDoubleBuffer = value;

                SetStyle(ControlStyles.OptimizedDoubleBuffer, value);
            }
        }
    }

Part 2

Manage WM_ERASEBKGND yourself, as the choice is otherwise 1) always off with AllPaintingInWmPaint, and no background paint on show, or 2) violating what double buffer expects where it would be always masking the WM_ERASEBKGND.

    protected override void WndProc(ref Message m)
    {
        switch (m.Msg)
        {
            case WM_ERASEBKGND:
                if (_hasPaintForeground && _isDoubleBuffer)
                    return;
        }
        base.WndProc(ref m);
    }

Part 3

You are now your own decider of what AllPaintingInWmPaint means.

In this case would want the initial messages to process like normal. When we knew for sure the .Net and DoubleBuffer side was finally kicking it, by seeing our first real paint happen, then turn WM_ERASEBKGND off for the duration.

    bool _hasPaintForeground;
    protected override void OnPaint(PaintEventArgs e)
    {
        // your paint code here, if any
        
        base.OnPaint(e);
        
        if (!_hasPaintForeground) // read cheaper than write every time
        {
            _hasPaintForeground = true;
        }
    }

Part 4

In my case, I also had originally gimped the OnBackground draw, which works if you are opaque drawing each element yourself in OnPaint. This allowed me to not have double buffer on for so long until I started following the clip and changed the timing so that I started also seeing the other WM_ERASEBKGND side effects and issues.

    protected override void OnPaintBackground(PaintEventArgs e)
    {                                   // Paint default background until first time paint foreground.
        if (!_hasPaintForeground)       //   This will kill background image but stop background flicker
        {                               //     and eliminate a lot of code, and need for double buffer.

            base.OnPaintBackground(e);
        }                               //   PaintBackground is needed on first show so no white background 
    }

I may not need this part anymore, but if my DoubleBuffer is off then I would. So long as I'm always painting opaque in OnPaint covering the whole draw Clip area.


Addendum:

In addition to all of that....

Separate issue with text render.

It looks like if I render only 250 x 42, like two rows and two text renders, which all occur in one OnPaint, verified with Diagnostics.Trace.WriteLine, then the text renders at least one monitor frame later, every single time. Making it look like the text is flashing. Is just 2x paint background single color then 2x paint text each for rows.

However, if I attempt to paint the whole client area of like 250 x 512 or whatever, like 17 rows, even though the e.Clip is exactly those two rows, because I'm the one that invalidated it, then no flicker of the text, 0 flicker.

There is either some timing issue or other side effect. But that's 17 chances instead of two, for at least one row to flicker text where the whole background is shown before the text renders, and it never happens. If I try to only render rows that are in the clip area it happens every time.

There is def something going with .Net or Windows. I tried with both g.DrawString and TextRenderer.DrawText and they both do it. Flicker if draw 2, not flicker if attempt to draw 17. Every time.

  • Maybe has something to do with drawing text near the mouse pointer, when OnPaint comes back too quickly?
  • Maybe if I draw enough things or OnPaint takes longer to come back, it's doing double buffer anyway? Dunno

So....

It's a good thing I went through this exercise with the original question.

I may choose to just render the whole client every time, but I'll never be able to do it the "right way" without something like my example code above.

Beeeaaar
  • 1,010
  • 9
  • 19