• Advertisement
Sign in to follow this  

Can't get flicker-free drawing to work in C#

This topic is 4139 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Hello, I'm making a little application that (so far) just allows the user to draw regions with the mouse. I'm trying to display a "ghost" region while the user is moving the mouse, but has not yet released it. The dilemma is that I have to constantly remove the previous ghost region and replace it with the new one. I simply use Invalidate() to remove the previous region, and just draw a new one over top. Naturally, this constant clearing of the screen produces a lot of flicker. So I went out and looked for a solution, and I found a class someone else wrote that allows double-buffer drawing. The idea is to draw to an off-screen buffer until you are ready to present it, and only then remove the previous frame. Unfortunately, the flicker did not go away. I have no clue why. Can someone please examine my code and tell me why the flicker is still there? It is unbearable to watch! Here's where I got the class. It includes instructions on how to use it. I tried to follow them, but since I'm drawing to a panel inside the form and not on the form itself, I had to make some compromises. It is possible that I messed up: Flicker Free Drawing Using GDI+ and C# Here's what that class looks like. I included it in my project: DBGraphics.cs
using System;
using System.Drawing;

namespace GDIDB
{
    /// <summary>
    /// Class to implement Double Buffering 
    /// NT Almond 
    /// 24 July 2003
    /// </summary>
    /// 
    public class DBGraphics
    {
        private Graphics graphics;
        private Bitmap memoryBitmap;
        private int width;
        private int height;

        /// <summary>
        /// Default constructor
        /// </summary>
        public DBGraphics()
        {
            width = 0;
            height = 0;
        }

        /// <summary>
        /// Creates double buffer object
        /// </summary>
        /// <param name="g">Window forms Graphics Object</param>
        /// <param name="width">width of paint area</param>
        /// <param name="height">height of paint area</param>
        /// <returns>true/false if double buffer is created</returns>
        public bool CreateDoubleBuffer(Graphics g, int width, int height)
        {

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

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

            if (width == 0 || height == 0)
                return false;


            if ((width != this.width) || (height != this.height))
            {
                this.width = width;
                this.height = height;

                memoryBitmap = new Bitmap(width, height);
                graphics = Graphics.FromImage(memoryBitmap);
            }

            return true;
        }


        /// <summary>
        /// Renders the double buffer to the screen
        /// </summary>
        /// <param name="g">Window forms Graphics Object</param>
        public void Render(Graphics g)
        {
            if (memoryBitmap != null)
                g.DrawImage(memoryBitmap, new Rectangle(0, 0, width, height), 0, 0, width, height, GraphicsUnit.Pixel);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns>true if double buffering can be achieved</returns>
        public bool CanDoubleBuffer()
        {
            return graphics != null;
        }

        /// <summary>
        /// Accessor for memory graphics object
        /// </summary>
        public Graphics g
        {
            get
            {
                return graphics;
            }
        }
    }
}

Here is what my own code looks like. I commented it quite thoroughly: Form1.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

// This namespace defines the classes necessary for double-buffer painting.
using GDIDB;

namespace HitTest1
{
    enum ProgramState
    {
        Idle,
        Drawing,

    }

    public partial class Form1 : Form
    {
        // Defines what the program is currently doing.
        ProgramState programState;

        // This variable is used to keep track of what point was the one
        // right before the current one when the user drags the mouse.
        Point lastPoint = new Point();
        
        // Used to reduce flicker in drawing by double buffering.
        private DBGraphics memGraphics;

        // This variable is used to record the path of the mouse as
        // the user draws a shape on the screen. The region is later
        // created using this path.
        System.Drawing.Drawing2D.GraphicsPath mousePath = new System.Drawing.Drawing2D.GraphicsPath();

        // CONSTRUCTOR
        // ===========
        public Form1()
        {
            InitializeComponent();

            // Initialize double-buffering.
            memGraphics = new DBGraphics();
            memGraphics.CreateDoubleBuffer(this.drawingPanel.CreateGraphics(),
                this.drawingPanel.ClientRectangle.Width, this.drawingPanel.ClientRectangle.Height);
        }

        private void drawingPanel_MouseDown(object sender, MouseEventArgs e)
        {
            // Make the program enter the drawings state, since we are now
            // in the process of drawing a new region.
            programState = ProgramState.Drawing;
            
            // Make the last point be equal to the current point.
            lastPoint.X = e.X;
            lastPoint.Y = e.Y;

            // Start tracking the mouse.
            mousePath.Reset();      // Clear the old path.
            mousePath.AddLine(e.X, e.Y, e.X, e.Y);
        }

        private void drawingPanel_MouseMove(object sender, MouseEventArgs e)
        {
            // Take action only if the mouse button is actually down,
            // and the program is in drawing mode.
            if (e.Button == MouseButtons.Left && programState == ProgramState.Drawing)
            {
                // We need access to the drawing panel in which the user is drawing.
                Panel thisPanel = (Panel)sender;
                Graphics graphics = thisPanel.CreateGraphics();

                // Clip the cursor to the panel - we restrict mouse movement
                // inside the drawing panel while the user is drawing. When
                // the user is done drawing the shape, the mouse is released.
                Cursor.Clip = new Rectangle(
                    this.PointToScreen(thisPanel.Location), thisPanel.Size);

                // Record the current position of the mouse so that the region
                // could be constructed later.
                Point currentPoint = new Point(e.X, e.Y);
                mousePath.AddLine(lastPoint, currentPoint);

                // Get ready for the next time the user moves the mouse -
                // make the current point the last one.
                lastPoint = currentPoint;

                // Cause the screen to be redrawn so that the current state of
                // the region could be displayed.
                thisPanel.Invalidate();
            }
        }

        private void drawingPanel_MouseUp(object sender, MouseEventArgs e)
        {
            // Take action only if we are in drawing mode.
            if (programState == ProgramState.Drawing)
            {
                // We are no longer drawing.
                programState = ProgramState.Idle;

                // We need access to the drawing panel.
                Panel thisPanel = (Panel)sender;
                Graphics graphics = thisPanel.CreateGraphics();

                // Construct the region from the recorded mouse path.
                Region region = new Region(mousePath);

                // Cause the panel to be redrawn.
                thisPanel.Invalidate();

                // Unclip the cursor - let it move outside the drawing panel now.
                Cursor.Clip = new Rectangle(0, 0, 0, 0);
            }
        }

        private void drawingPanel_Paint(object sender, PaintEventArgs e)
        {
            if (memGraphics.CanDoubleBuffer())
            {
                // Fill in the background.
                memGraphics.g.FillRectangle(new SolidBrush(SystemColors.Window),
                    e.ClipRectangle.X, e.ClipRectangle.Y, e.ClipRectangle.Width,
                    e.ClipRectangle.Height);

                // TO DO: HANDLE ALL DRAWING HERE:
                // ===============================

                // Construct a region out of the points recorded so far.
                Region region = new Region(mousePath);

                // Fill the region with a color.
                if (programState == ProgramState.Drawing)
                {
                    // If the user is still drawing, fill the region in using
                    // the color "wheat".
                    memGraphics.g.FillRegion(System.Drawing.Brushes.Wheat, region);
                }
                else if (programState == ProgramState.Idle)
                {
                    // If the user is done drawing, fill the region in using
                    // the color "dark green".
                    memGraphics.g.FillRegion(System.Drawing.Brushes.DarkGreen,
                        region);
                }

                // ===============================

                // Now that we are done drawing, render to the form.
                memGraphics.Render(e.Graphics);
            }
        }        
    }
}

Here is the file that is generated by Visual Studio. It simply defines what the form looks like. It is needed if you want to compile my program. Form1.Designer.cs
namespace HitTest1
{
    partial class Form1
    {
        /// <summary>
        /// Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            this.drawingPanel = new System.Windows.Forms.Panel();
            this.SuspendLayout();
            // 
            // drawingPanel
            // 
            this.drawingPanel.BackColor = System.Drawing.SystemColors.Window;
            this.drawingPanel.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
            this.drawingPanel.Location = new System.Drawing.Point(67, 83);
            this.drawingPanel.Name = "drawingPanel";
            this.drawingPanel.Size = new System.Drawing.Size(586, 312);
            this.drawingPanel.TabIndex = 0;
            this.drawingPanel.MouseDown += new System.Windows.Forms.MouseEventHandler(this.drawingPanel_MouseDown);
            this.drawingPanel.MouseMove += new System.Windows.Forms.MouseEventHandler(this.drawingPanel_MouseMove);
            this.drawingPanel.Paint += new System.Windows.Forms.PaintEventHandler(this.drawingPanel_Paint);
            this.drawingPanel.MouseUp += new System.Windows.Forms.MouseEventHandler(this.drawingPanel_MouseUp);
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(665, 407);
            this.Controls.Add(this.drawingPanel);
            this.Name = "Form1";
            this.Text = "Form1";
            this.ResumeLayout(false);

        }

        #endregion

        private System.Windows.Forms.Panel drawingPanel;
    }
}

I would appreciate any help!

Share this post


Link to post
Share on other sites
Advertisement
If you're using .NET 2.0, then you don't need a class to handle double-buffering. Controls now have a DoubleBuffer property that takes care of that for you.

Also, note that when a control is invalidated, by default a WM_ERASEBKGRND message is also sent to the control. This clears the entire control which causes flicker. If you set the DoubleBuffer property to true, then the AllPaintingInWmPaint style is also set. This causes the WM_ERASEBKGRND message to be ignored and helps eliminate the flicker.

Finally, instead of invalidating the entire panel, try using the overload that takes a rectangle parameter and just invalidate the part that needs to be redrawn.

Share this post


Link to post
Share on other sites
Quote:
Original post by Dave Hunt
If you're using .NET 2.0, then you don't need a class to handle double-buffering. Controls now have a DoubleBuffer property that takes care of that for you.

Also, note that when a control is invalidated, by default a WM_ERASEBKGRND message is also sent to the control. This clears the entire control which causes flicker. If you set the DoubleBuffer property to true, then the AllPaintingInWmPaint style is also set. This causes the WM_ERASEBKGRND message to be ignored and helps eliminate the flicker.

Finally, instead of invalidating the entire panel, try using the overload that takes a rectangle parameter and just invalidate the part that needs to be redrawn.


Thanks for the help. In the form designer, I found a property called "DoubleBuffered" which I changed to "True". Then, I added this line to the constructor:
this.Setstyle(Controlstyles.AllPaintingInWmPaint | Controlstyles.UserPaint | Controlstyles.DoubleBuffer, true);
I got rid of DBGraphics.cs. And finally, I started keeping track of exactly which areas need to be invalidated to avoid redrawing everything.

But like a nightmare, the flicker persists! I *think* it is better now, but I can't be sure.

Here's the modified code:

Form1.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace HitTest1
{
enum ProgramState
{
Idle,
Drawing,

}

public partial class Form1 : Form
{
// Defines what the program is currently doing.
ProgramState programState;

// This variable is used to keep track of what point was the one
// right before the current one when the user drags the mouse.
Point lastPoint = new Point();

// This variable is used to record the path of the mouse as
// the user draws a shape on the screen. The region is later
// created using this path.
System.Drawing.Drawing2D.GraphicsPath mousePath = new System.Drawing.Drawing2D.GraphicsPath();

// To reduce the flicker, we would like to invalidate only a part
// of the drawing panel when redrawing needs to be done. So we keep
// track of the area that needs to be redrawn based on the where the user
// is currently drawing. The area that needs to be redrawn is the rectangle
// between the start point and the point where the user's mouse is now.
Rectangle invalidateBounds = new Rectangle();
Point startPoint = new Point();

// CONSTRUCTOR
// ===========
public Form1()
{
InitializeComponent();

// Set up to use double-buffering.
this.SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true);
}

private void drawingPanel_MouseDown(object sender, MouseEventArgs e)
{
// Make the program enter the drawings state, since we are now
// in the process of drawing a new region.
programState = ProgramState.Drawing;

// Make the last point be equal to the current point.
lastPoint.X = e.X;
lastPoint.Y = e.Y;

// Begin tracking the size of the region (for redrawing).
startPoint = new Point(e.X, e.Y);
invalidateBounds.X = e.X;
invalidateBounds.Width = 0;
invalidateBounds.Y = e.Y;
invalidateBounds.Height = 0;

// Start tracking the mouse.
mousePath.Reset(); // Clear the old path.
mousePath.AddLine(e.X, e.Y, e.X, e.Y);
}

private void drawingPanel_MouseMove(object sender, MouseEventArgs e)
{
// Take action only if the mouse button is actually down,
// and the program is in drawing mode.
if (e.Button == MouseButtons.Left && programState == ProgramState.Drawing)
{
// We need access to the drawing panel in which the user is drawing.
Panel thisPanel = (Panel)sender;
Graphics graphics = thisPanel.CreateGraphics();

// Clip the cursor to the panel - we restrict mouse movement
// inside the drawing panel while the user is drawing. When
// the user is done drawing the shape, the mouse is released.
//
// TEMPORARILY DISABLED FOR DEBUGGING
//Cursor.Clip = new Rectangle(
// this.PointToScreen(thisPanel.Location), thisPanel.Size);

// Record the current position of the mouse so that the region
// could be constructed later.
Point currentPoint = new Point(e.X, e.Y);
mousePath.AddLine(lastPoint, currentPoint);

// Update the size of the rectangle that needs to be
// invalidated. Again: the area that needs to be redrawn
// is the rectangle between the start point and the point
// where the user's mouse is now.
if (e.X < startPoint.X)
{
invalidateBounds.X = e.X;
invalidateBounds.Width = startPoint.X - e.X;
}
else
{
invalidateBounds.X = startPoint.X;
invalidateBounds.Width = e.X - startPoint.X;
}
if (e.Y < startPoint.Y)
{
invalidateBounds.Y = e.Y;
invalidateBounds.Height = startPoint.Y - e.Y;
}
else
{
invalidateBounds.Y = startPoint.Y;
invalidateBounds.Height = e.Y - startPoint.Y;
}

// Get ready for the next time the user moves the mouse -
// make the current point the last one.
lastPoint = currentPoint;

// Cause the screen to be redrawn so that the current state of
// the region could be displayed. Only invalidate the region
// that actually needs to be redrawn
thisPanel.Invalidate(invalidateBounds);
}
}

private void drawingPanel_MouseUp(object sender, MouseEventArgs e)
{
// Take action only if we are in drawing mode.
if (programState == ProgramState.Drawing)
{
// We are no longer drawing.
programState = ProgramState.Idle;

// We need access to the drawing panel.
Panel thisPanel = (Panel)sender;
Graphics graphics = thisPanel.CreateGraphics();

// Construct the region from the recorded mouse path.
Region region = new Region(mousePath);

// Cause the panel to be redrawn.
thisPanel.Invalidate();

// Unclip the cursor - let it move outside the drawing panel now.
Cursor.Clip = new Rectangle(0, 0, 0, 0);
}
}

private void drawingPanel_Paint(object sender, PaintEventArgs e)
{
// Construct a region out of the points recorded so far.
Region region = new Region(mousePath);

// Fill the region with a color.
if (programState == ProgramState.Drawing)
{
// If the user is still drawing, fill the region in using
// the color "wheat".
e.Graphics.FillRegion( System.Drawing.Brushes.Wheat,
region );
}
else if (programState == ProgramState.Idle)
{
// If the user is done drawing, fill the region in using
// the color "dark green".
e.Graphics.FillRegion( System.Drawing.Brushes.DarkGreen,
region );
}
}
}
}





Visual Studio-generated file:
Form1.Designer.cs

namespace HitTest1
{
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;

/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}

#region Windows Form Designer generated code

/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.drawingPanel = new System.Windows.Forms.Panel();
this.SuspendLayout();
//
// drawingPanel
//
this.drawingPanel.BackColor = System.Drawing.SystemColors.Window;
this.drawingPanel.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.drawingPanel.Location = new System.Drawing.Point(67, 83);
this.drawingPanel.Name = "drawingPanel";
this.drawingPanel.Size = new System.Drawing.Size(586, 312);
this.drawingPanel.TabIndex = 0;
this.drawingPanel.MouseDown += new System.Windows.Forms.MouseEventHandler(this.drawingPanel_MouseDown);
this.drawingPanel.MouseMove += new System.Windows.Forms.MouseEventHandler(this.drawingPanel_MouseMove);
this.drawingPanel.Paint += new System.Windows.Forms.PaintEventHandler(this.drawingPanel_Paint);
this.drawingPanel.MouseUp += new System.Windows.Forms.MouseEventHandler(this.drawingPanel_MouseUp);
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(665, 407);
this.Controls.Add(this.drawingPanel);
this.DoubleBuffered = true;
this.Name = "Form1";
this.Text = "Form1";
this.ResumeLayout(false);

}

#endregion

private System.Windows.Forms.Panel drawingPanel;
}
}





Again, I would appreciate any more help!

Share this post


Link to post
Share on other sites
Try setting the DoubleBuffer and other properties on the panel, rather than the whole form. Also, setting the DoubleBuffer property to true sets the OptimizedDoubleBuffer and AllPaintingInWmPaint styles for you, so you don't need to set them. You still need to set the UserPaint style, though.

I suspect that what you are seeing is caused by the form still sending WM_ERASEBKGRND messages to the controls. Since the controls themselves don't have the AllPaintingInWmPaint style set, they are still erasing their backgrounds. Setting the property directly on the panel should take care of that.

That is just conjecture, so your mileage may vary.

Share this post


Link to post
Share on other sites
Hello,

Firstly, sorry that I dropped this thread for a week. I had quite a bit of school work lately, so I did not do any programming.

Secondly, I looked into the properties for the panel, but it appears that there is no option for double buffering. In fact, I've confirmed that double buffering on a panel requires inheriting from the Panel class and creating your own custom control: http://www.thescripts.com/forum/post925696-2.html.

Which brings up another problem: I did exactly what I just described. I inherited the panel class and defined a panel that can do double-buffered painting. Now, how can I add this custom panel to my form? The only thing I can think of is to modify the actual code; however, Visual Studio would prefer me not to touch its Form Designer-generated code.

Searching for how to solve this, I came across that I need to create a Windows Control Library project. However, upon creating a new project, in the skeleton application that was generated, the custom control derived from the UserControl class:

using ...;
namespace BufferPanel
{
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
}
}


I suspect that it is not OK to simply change UserControl to Panel, but that's what I kind of need to do here. I need to inherit from the Panel class to obtain access to its protected Setstyle() method.

So what can I do here? What is usually done?

Share this post


Link to post
Share on other sites
There is no problem with deriving your custom control from a Panel. Just change UserControl to Panel and go for it.

Share this post


Link to post
Share on other sites
I've found that the DoubleBuffer property on controls doesn't really get rid of flicker. The .NET framework isn't very consistent when it comes to rendering (the ListBox being particularly crap).

After much experimentation, I've found that deriving from UserControl and overriding the OnPaint and OnPaintBackground methods. My OnPaintBackground does nothing (not even call base class) and OnPaint uses the BufferedGraphics object to do the rendering. Lastly, I've implemented my own version of all the controls I need. The result does look good but was quite a bit of work.

Skizz

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement