13

I am working on kind of drawing program but I have a problem with flickering while moving a mouse cursor while drawing a rubberband line. I hope you can help me to remove that flickering of line, here is the code:

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

namespace GraphicsTest
{
    public partial class Form1 : Form
    {
        int xFirst, yFirst;
        Bitmap bm = new Bitmap(1000, 1000);
        Graphics bmG;
        Graphics xG;
        Pen pen = new Pen(Color.Black, 1);
        bool draw = false;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            bmG = Graphics.FromImage(bm);
            xG = this.CreateGraphics();
            bmG.Clear(Color.White);
        }

        private void Form1_MouseDown(object sender, MouseEventArgs e)
        {
            xFirst = e.X;
            yFirst = e.Y;
            draw = true;
        }

        private void Form1_MouseUp(object sender, MouseEventArgs e)
        {
            bmG.DrawLine(pen, xFirst, yFirst, e.X, e.Y);
            draw = false;
            xG.DrawImage(bm, 0, 0);
        }

        private void Form1_MouseMove(object sender, MouseEventArgs e)
        {
            if (draw)
            {
                xG.DrawImage(bm, 0, 0);
                xG.DrawLine(pen, xFirst, yFirst, e.X, e.Y);
            }
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            xG.DrawImage(bm, 0, 0);
        }
    }
}
OregonGhost
  • 23,359
  • 7
  • 71
  • 108
Jeff
  • 725
  • 2
  • 7
  • 11

4 Answers4

35

First don't use CreateGraphics() unless you absolutely have to. Bind an event handler to OnPaint and call Invalidate() when you want to refresh the surface.

If you don't want it to flicker you'll need to double buffer your drawing surface. The easiest way to do this is to set your form's DoubleBuffered property to True.

I would highly recommend if you plan on extending this to do your drawing to the PictureBox control. PictureBox is double-buffered by default and allows you to control your drawing region much more simply.

In code:

public partial class Form1 : Form
    {
    int xFirst, yFirst;
    Bitmap bm = new Bitmap(1000, 1000);
    Graphics bmG;
    Pen pen = new Pen(Color.Black, 1);
    bool draw = false;

    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        bmG = Graphics.FromImage(bm);
        bmG.Clear(Color.White);
    }

    private void Form1_MouseDown(object sender, MouseEventArgs e)
    {
        xFirst = e.X;
        yFirst = e.Y;
        draw = true;
    }

    private void Form1_MouseUp(object sender, MouseEventArgs e)
    {
        bmG.DrawLine(pen, xFirst, yFirst, e.X, e.Y);
        draw = false;
        Invalidate();
    }

    private void Form1_MouseMove(object sender, MouseEventArgs e)
    {
        if (draw)
        {
            Invalidate();
        }
    }

    private void Form1_Paint(object sender, PaintEventArgs e)
    {
        if (draw) {
            e.Graphics.DrawImage(bm, 0, 0);
            e.Graphics.DrawLine(pen, xFirst, yFirst, e.X, e.Y);
        } else {
            e.Graphics.DrawImage(bm, 0, 0);
        }
    }
}

Edit:

Another issue, you are creating a private Pen member. Pens (and Brushes, as well as many GDI+ objects) represent handles to unmanaged objects that need to be disposed otherwise your program will leak. Either wrap them in using statements (the preferred and exception-safe way) or explicitly dispose of them in the form's Dispose method.

Alternatively in System.Drawing you can access some pre-built Pens and Brushes that don't need to be (and shouldn't be) disposed. Use them like:

    private void Form1_Paint(object sender, PaintEventArgs e)
    {
        if (draw) {
            e.Graphics.DrawImage(bm, 0, 0);
            e.Graphics.DrawLine(Pens.Black, xFirst, yFirst, e.X, e.Y);
        } else {
            e.Graphics.DrawImage(bm, 0, 0);
        }
    }
Ron Warholic
  • 9,994
  • 31
  • 47
  • I fixed a small issue with the code, you can get rid of your xG object and use the `e.Graphics` property of the PaintEventArgs in the paint event to get the graphics context of your form. – Ron Warholic Apr 09 '10 at 16:11
  • Yes, it works. Thanks a lot. Now I understand the use of Invalidate() too. Nice. – Jeff Apr 09 '10 at 17:17
  • I wish I could upvote you 1000^1000^1000 times at least! This saved my life man! :D – Pratik Deoghare Apr 23 '10 at 12:43
  • 1
    Trying your code on VS2012 targeting .NET Framework 4.0 and it doesn't work. Get errors in the Form1_Paint event where you access e.X and e.Y, which are not valid for some reason. – HK1 Feb 22 '14 at 03:39
  • Is there a way to do the drawing beforehand (outside of the Paint event handler), and only do the actual drawing to screen in the Paint Handler? Suppose I have a significant amount of calculations that need to occur while generating the image, and I'd like this to occur in the background. Can this be done or am I going to have to use some other construct, perhaps Bitmap? – smead May 29 '14 at 23:53
  • Use `ThreadPool.QueueUserWorkItem` to push your drawing to a background thread inside your mouse event handler. Instead of drawing directly to the control create a `Bitmap` object, draw to that in your thread with `Graphics.FromImage` then `BeginInvoke` back to the main thread to trigger the invalidate on the control. The paint event should then just draw your bitmap. – Ron Warholic Jun 02 '14 at 13:23
11

The reason it is flickering is that you are drawing the background (which is immediately displayed on screen, wiping away the line) and then superimpose the line. Thus the line keeps disappearing and appearing, giving a flickering display.

The best solution to this is called Double Buffering. What you do is draw the whole image to an "offscreen" bitmap, and only show it on the screen when it is completed. Because you only ever display the completed image, there is no flickering effect. You should just be able to set this.DoubleBuffered = true to get WinForms to do all the hard work for you.

NB: You shouldn't really be drawing outside of your paint handler - ideally, you should Invalidate() the area that needs redrawing, and then your paint handler will redraw just that area (with any overlaid lines etc as needed).

Jason Williams
  • 56,972
  • 11
  • 108
  • 137
2

Fixed and working code.

public partial class Form1 : Form
{
    int x1, y1, x2, y2;
    bool drag = false;

    Bitmap bm = new Bitmap(1000, 1000);
    Graphics bmg;


    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        bmg = Graphics.FromImage(bm);
    }

    private void pictureBox_MouseDown(object sender, MouseEventArgs e)
    {
        drag = true;
        x1 = e.X;
        y1 = e.Y;
    }

    private void pictureBox_MouseUp(object sender, MouseEventArgs e)
    {
        drag = false;

        bmg.DrawLine(Pens.Black, x1, y1, e.X, e.Y);
        pictureBox.Invalidate();
    }

    private void pictureBox_MouseMove(object sender, MouseEventArgs e)
    {
        if (drag)
        {
            x2 = e.X;
            y2 = e.Y;
            pictureBox.Invalidate();
        }
    }

    private void pictureBox_Paint(object sender, PaintEventArgs e)
    {
        if (drag) {
            e.Graphics.DrawImage(bm, 0, 0);
            e.Graphics.DrawLine(Pens.Black, x1, y1, x2, y2);            
        }
        else {
            e.Graphics.DrawImage(bm, 0, 0);
        }
    }
}
Jeff
  • 725
  • 2
  • 7
  • 11
1

I Use this to manage the double buffering into a panel:

myPanel.GetType().GetMethod("SetStyle",
    System.Reflection.BindingFlags.Instance |
    System.Reflection.BindingFlags.NonPublic).Invoke(myPanel,
        new object[]
        {
            System.Windows.Forms.ControlStyles.UserPaint | 
            System.Windows.Forms.ControlStyles.AllPaintingInWmPaint |
            System.Windows.Forms.ControlStyles.DoubleBuffer, true
        });
Dharman
  • 30,962
  • 25
  • 85
  • 135
ghiboz
  • 7,863
  • 21
  • 85
  • 131