6
votes

I have two problems with an own user control which uses bitmaps:

  1. It flickers if it's redrawn via .NET's 'Refresh' method.
  2. It has a bad performance.

The control consists of three bitmaps:

  • A static background image.
  • A rotating rotor.
  • Another image depending on rotor angle.

All used bitmaps have a resolution of 500x500 pixels. The control works like this: https://www.dropbox.com/s/t92gucestwdkx8z/StatorAndRotor.gif (it's a gif animation)

The user control should draw itself everytime it gets a new rotor angle. Therefore, it has a public property 'RotorAngle' which looks like this:

public double RotorAngle
{
    get { return mRotorAngle; }
    set
    {
        mRotorAngle = value;
        Refresh();
    }
}

Refresh raises the Paint event. The OnPaint event handler looks like this:

private void StatorAndRotor2_Paint(object sender, PaintEventArgs e)
{
    // Draw the three bitmaps using a rotation matrix to rotate the rotor bitmap.
    Draw((float)mRotorAngle);
}

But when I use this code - which works well in other own user controls - the user control is not drawn at all if control is double buffered via SetStyle(ControlStyles.OptimizedDoubleBuffer, true). If I don't set this flag to true, the control flickers when being redrawn.

In control constructor I set:

SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.ContainerControl, false);
// User control is not drawn if "OptimizedDoubleBuffer" is true.
// SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
SetStyle(ControlStyles.ResizeRedraw, true);
SetStyle(ControlStyles.SupportsTransparentBackColor, true);

First, I thought it flickers because the background is cleared everytime the control is drawn. Therefore, I set SetStyle(ControlStyles.AllPaintingInWmPaint, true). But it didn't help.

So, why does it flicker? Other controls work very well with this setup. And why is the controly not drawn if SetStyle(ControlStyles.OptimizedDoubleBuffer, true).

I found out that the control does not flicker if I invoke my Draw method directly after changing the property RotorAngle:

public float RotorAngle
{
    get { return mRotorAngle; }
    set
    {
        mRotorAngle = value;
        Draw(mRotorAngle);
    }
}

But this results in a very bad performance, especially in full screen mode. It's not possible to update the control every 20 milliseconds. You can try it yourself. I will attach the complete Visual Studio 2008 solution below.

So, why it is such a bad performance? It's no problem to update other (own) controls every 20 milliseconds. Is it really just due to the bitmaps?

I created a simple visual Visual Studio 2008 solution to demonstrate the two problems: https://www.dropbox.com/s/mckmgysjxm0o9e0/WinFormsControlsTest.zip (289,3 KB)

There is an executable in directory bin\Debug.

Thanks for your help.

5
I can't go into all the details now, but you need to look at double buffering. Fortunately it's very simple in 2008+ The way you are doing the refresh in the property is bad too.Ian
Please don't double-buffer if your application might be used over remote desktop - see this link. You can check if the current session is remote desktop with: if (SystemInformation.TerminalServerSession) ...Bridge

5 Answers

4
votes

First, per LarsTech's answer, you should use the Graphics context provided in the PaintEventArgs. By calling CreateGraphics() inside the Paint handler, you prevent OptimizedDoubleBuffer from working correctly.

Second, in your SetStyle block, add:

SetStyle( ControlStyles.Opaque, true );

... to prevent the base class Control from filling in the background color before calling your Paint handler.

I tested this in your example project, it seemed to eliminate the flickering.

4
votes

Do not use CreateGraphics, but use the Graphic object that was passed to you from the paint event:

I changed it like so and added a clear since resizing would show the ghost image:

private void Draw(float rotorAngle, Graphics graphics)
{
  graphics.Clear(SystemColors.Control);
  graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;

  // yada-yada-yada

  // do not dispose since you did not create it:
  // graphics.Dispose();
}

called from:

private void StatorAndRotor2_Paint(object sender, PaintEventArgs e)
{
  Draw((float)mRotorAngle, e.Graphics);
}

In the constructor, turn on double-buffering, but I think the transparency isn't needed:

SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.ContainerControl, false);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
SetStyle(ControlStyles.ResizeRedraw, true);
//SetStyle(ControlStyles.SupportsTransparentBackColor, true);
2
votes

Drawing at screen on Paint event is a heavy operation. Doing paint on memory buffer is comparatively very fast.

Double buffering will improve the performance. Instead of drawing on paint graphics, create a new graphics, and do all drawing on it.

Once bitmap drawing is done, copy the entire bitmap to e.Graphics received from PaintEventArgs

Flicker free drawing using GDI+ and C#

0
votes

For my answer I took inspiration from https://stackoverflow.com/a/2608945/455904 and I nabbed stuff from LarsTechs answer above.

To avoid having to regenerate the complete image on all OnPaints you can use a variable to hold the generated image.

private Bitmap mtexture;

Use Draw() to generate the texture

private void Draw(float rotorAngle)
{
    using (var bufferedGraphics = Graphics.FromImage(mtexture))
    {
        Rectangle imagePosition = new Rectangle(0, 0, Width, Height);
        bufferedGraphics.DrawImage(mStator, imagePosition);
        bufferedGraphics.DrawImage(RotateImage(mRotor, mRotorAngle), imagePosition);

        float normedAngle = mRotorAngle % cDegreePerFullRevolution;

        if (normedAngle < 0)
            normedAngle += cDegreePerFullRevolution;

        if (normedAngle >= 330 || normedAngle <= 30)
            bufferedGraphics.DrawImage(mLED101, imagePosition);
        if (normedAngle > 30 && normedAngle < 90)
            bufferedGraphics.DrawImage(mLED001, imagePosition);
        if (normedAngle >= 90 && normedAngle <= 150)
            bufferedGraphics.DrawImage(mLED011, imagePosition);
        if (normedAngle > 150 && normedAngle < 210)
            bufferedGraphics.DrawImage(mLED010, imagePosition);
        if (normedAngle >= 210 && normedAngle <= 270)
            bufferedGraphics.DrawImage(mLED110, imagePosition);
        if (normedAngle > 270 && normedAngle < 330)
            bufferedGraphics.DrawImage(mLED100, imagePosition);
    }
}

Have the override of OnPaint draw the texture on your control

protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e);
    Rectangle imagePosition = new Rectangle(0, 0, Width, Height);
    e.Graphics.DrawImage(mtexture, imagePosition);
}

Override OnInvalidated() to Draw() the texture when needed

protected override void OnInvalidated(InvalidateEventArgs e)
{
    base.OnInvalidated(e);

    if (mtexture != null)
    {
        mtexture.Dispose();
        mtexture = null;
    }

    mtexture = new Bitmap(Width, Height);
    Draw(mRotorAngle);
}

Instead of calling Draw invalidate the image. This will cause it to repaint using OnInvalidated and OnPaint.

public float RotorAngle
{
    get { return mRotorAngle; }
    set
    {
        mRotorAngle = value;
        Invalidate();
    }
}

I hope I got all of it in there :)

0
votes

Thank you very much for your help.

Flickering is solved. :)

Now, I follow LarsTech's suggestion and use the Graphics object from PaintEventArgs.

Thanks lnmx for the hint that CreateGraphics() inside Paint handler prevents the correct function of OptimizedDoubleBuffer. This explains the flickering problem even if OptimizedDoubleBuffer was enabled. I didn't know that and I also haven't found this at MSDN Library. In my previous controls, I have also used the Graphics object from PaintEventArgs.

Thanks Sallow for your efforts. I will test your code today and I will give feedback. I hope this will improve performance because there is still a performance issue - despite correct double buffering.

There was still another performance problem in my original code.

Changed

graphics.DrawImage(mStator, imagePosition);
graphics.DrawImage(RotateImage(mRotor, rotorAngle), imagePosition);

to

graphics.DrawImage(mStator, imagePosition);
Bitmap rotatedImage = RotateImage(mRotor, rotorAngle);
graphics.DrawImage(rotatedImage, imagePosition);
rotatedImage.Dispose(); // Important, otherwise the RAM will be flushed with bitmaps.