Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Drawing a rubber-band line in GDI+

0.00/5 (No votes)
11 Dec 2001 2  
Drawing a self-erasing line or a rubber-band line from a base-point to the current mouse point in GDI+ seemed to be impossible - here's how to do it.

Sample Image - RubberBandLine.jpg

Introduction

Drawing a self-erasing line or a rubber-band line from a base-point to the current mouse point in GDI+ seemed to be impossible. This is the famous and standard XOR'ing problem that has been written about many times in the past. The XOR mode is set in the Win32 API by calling SetROP2. The only analog to the SetROP2 function in GDI+ is the Graphics.CompositingMode property which supports all of two modes: SourceOver and SourceCopy. Neither of these allow you to draw a line, then 'undraw' it. This article explains what you have to do to draw and undraw lines. Doing the same thing for other shapes is a simple extension of the same techniques.

Background

I have been interested in graphics programming for as long as I have been programming, which is climbing well above the number of fingers I have with which to do it. A couple years ago I built a library of routines to sit on top of GDI to allow me to draw objects using real world units, such as inches and meters. Basically, these routines acted like a little CAD engine, hiding the brutal weirdness of pixels, 4th quadrant coordinate systems, MoveTo and LineTo functions, and circle drawing with ellipses drawn within the rectangular area bounded by the top-left point to the bottom-right point - silly APIs that have little to do with real world objects.

I started porting this code to C# and .NET using the Visual Studio 7.0 Beta 2 version. Lots of fun there, and the subject of another article, I'm sure. (Turns out, C++ code written using templates and STL does not port very easily, but I persevered.) While I was doing this I decided that I wanted to test the classes that I was building by adding buttons to a dialog to set a drawing mode: [Line], [Circle], [Rectangle] and so on. Each of these buttons would, when pressed, instantiate an object to perform 'rubber-banding', or repeatedly drawing an object from a base point to the currently tracked mouse position, then undrawing it before drawing the next one.

This turned out to be much harder than I expected.

The RubberBandLine Object

The RubberBandLine class handles all of the mouse events for drawing and tracking the motion of the mouse. A variable of type RubberBandLine is added as a private member of the class to the form like this:

private RubberBandLine rubberBandLine = null;

When the form is instantiated, the rubberBandLine member is created and initialized like this:

if (rubberBandLine == null)
{
    rubberBandLine = new RubberBandLine();
    rubberBandLine.Color = Color.Firebrick;
    rubberBandLine.Width = 1;
    rubberBandLine.DashStyle = DashStyle.Dash;
    rubberBandLine.PictureBox = pictureBox1;
}
 
if (rubberBandLine != null)
{
    rubberBandLine.InitializeComponent();
}

This code creates a new RubberBandLine object and assigns it to the member we declared earlier, then sets some properties. The most important property is the PictureBox property. This is critical to making this work. The PictureBox has an Image member which is what we use to capture the screen so that we can restore it correctly. Given that the creation and property settings are successful, the InitializeComponent() method is called. This is reproduced here.

/// <summary>

/// Instance variable for handling the mouse up event 

/// for a single rubber-band object.

/// </summary>

protected System.Windows.Forms.MouseEventHandler mu = null;
/// <summary>

/// Instance variable for handling the mouse move event for 

/// a single rubber-band object.

/// </summary>

protected System.Windows.Forms.MouseEventHandler mm = null;
/// <summary>

/// Instance variable for handling the mouse down event for

/// a single rubber-band object.

/// </summary>

protected System.Windows.Forms.MouseEventHandler md = null;
 
public void InitializeComponent()
{
    this.mu = new System.Windows.Forms.MouseEventHandler(
        this.pictureBox_MouseUp);
    this.mm = new System.Windows.Forms.MouseEventHandler(
        this.pictureBox_MouseMove);
    this.md = new System.Windows.Forms.MouseEventHandler(
        this.pictureBox_MouseDown);
    this.pictureBox.MouseUp += this.mu;
    this.pictureBox.MouseMove += this.mm;
    this.pictureBox.MouseDown += this.md;
}

Why are the MouseEventHandlers not inlined into the MouseUp += xxx statements, you ask? With a single rubber-banding class this is a very valid question, but as soon as you extend this to rubber-banding anything else, there must be a way to turn off the event handling, or you will get both, or all of the handlers operating at the same time. (In fact, while I was fine-tuning this code, I inadvertently called InitializeComponent() twice, and saw exactly this behavior, since the UnInitializeComponent() method was only being called once. In addition this shows that the += operator for events can be reversed using the -= operator, something that is exceedingly light in all of the books on C# and .NET that I have perused. The caveat, I think, is that you have to cache the original object used in the += operation for use in backing it out. A small price to pay, IMHO.) Calling the UnInitializeComponent() method causes the rubber-banding to be suspended until InitializeComponent() is called again.

public void UnInitializeComponent()
{
    this.pictureBox.MouseUp -= this.mu;
    this.pictureBox.MouseMove -= this.mm;
    this.pictureBox.MouseDown -= this.md;
    mu = null;
    mm = null;
    md = null;
}

On a mouse down event a snapshot of the current Image for the PictureBox is taken. This will be used to create a TextureBrush. This TextureBrush will be used to create a pen that can draw the background image. No matter what shape you draw, that shape will lay down the pixels that existed when the snapshot was made. This was the really tricky thing to get right! This means that if you draw a line in black across a picture of your best friend, then draw the same exact line with this TextureBrush pen, the result will be as if you did neither. Exactly what we wanted!

The basic algorithm for taking the snapshot came from Mike Gold in response to an email I sent him asking him for his ideas about how to do this, and is paraphrased below.

Rectangle r = pictureBox.ClientRectangle;
    Graphics g1 = pictureBox.CreateGraphics();
    Image i = new Bitmap(r.Width, r.Height, g1);
    pictureBox.Image = i;
    Graphics g2 = Graphics.FromImage(i);
    IntPtr dc1 = g1.GetHdc();
    IntPtr dc2 = g2.GetHdc();
    
    BitBlt(dc2, 0, 0, r.Width, r.Height, dc1, 
        0, 0, 0x00CC0020 /* dest = source*/);
    
    g1.ReleaseHdc(dc1);
    g2.ReleaseHdc(dc2);

See the source below for more fully documented notes on this algorithm.

The Rest of the Story

The rest is fairly simple. Compile the project, run it, then pick and drag in the PictureBox to draw the loveliest lines you'd ever want to draw! :-) As long as the mouse is moving and you have not let up on the mouse, a line will be drawn from the point at which the mouse-down event occurred, and lines will be drawn and undrawn using this technique until you release the mouse button. Repeat ad nausea. Rejoice that the minions at Microsoft have once again been defeated - we WILL have our XOR'ing, thank you very much!

I hope that this is as interesting to others as it was to me.

The RubberBandLine Object Source

using System;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Diagnostics;

namespace RubberBandLine_demo
{
    /// <summary>

    /// Summary description for RubberBandLine.

    /// </summary>

    public class RubberBandLine
    {
        public RubberBandLine()
        {
            Debug.WriteLine("Creating RubberBandLine");
        }

        public void InitializeComponent()
        {
            this.mu = new MouseEventHandler(
                this.pictureBox_MouseUp);
            this.mm = new MouseEventHandler(
                this.pictureBox_MouseMove);
            this.md = new MouseEventHandler(
                this.pictureBox_MouseDown);
            this.pictureBox.MouseUp += this.mu;
            this.pictureBox.MouseMove += this.mm;
            this.pictureBox.MouseDown += this.md;
        }

        public void UnInitializeComponent()
        {
            this.pictureBox.MouseUp -= this.mu;
            this.pictureBox.MouseMove -= this.mm;
            this.pictureBox.MouseDown -= this.md;
            mu = null;
            mm = null;
            md = null;
        }

        /// <summary>

        /// Private variable to hold the width of the object 

        /// being drawn.

        /// </summary>

        private int width = 1;
    
        /// <summary>

        /// Width is the attribute holding the width of the 

        /// object being drawn.

        /// The Width attribute is read/write.

        /// </summary>

        public int Width
        {
            get { return width; }
            set { width = value; }
        }

        /// <summary>

        /// Private variable to hold the color of the 

        /// object being drawn.

        /// </summary>

        private Color color = Color.FromArgb(255, 0, 0, 0);

        /// <summary>

        /// Color is the attribute holding the color of the

        /// object being drawn.

        /// The Color attribute is read/write.

        /// </summary>

        public Color Color
        {
            get { return color; }
            set { color = value; }
        }

        /// <summary>

        /// Private variable to hold the color of the object

        /// being drawn.

        /// </summary>

        private DashStyle dashStyle = DashStyle.Dot;

        /// <summary>

        /// DashStyle is the attribute holding 

        /// the line style of the object being drawn.

        /// The DashStyle attribute is read/write.

        /// </summary>

        public DashStyle DashStyle
        {
            get { return dashStyle; }
            set { dashStyle = value; }
        }

        /// <summary>

        /// Protected variable to hold the texture brush 

        /// that is used to create the erasing pen for the 

        /// object being drawn.

        /// </summary>

        protected TextureBrush staticTextureBrush = null;
        /// <summary>

        /// Protected variable to hold the 

        /// erasing pen for the object being drawn.

        /// </summary>

        protected Pen staticPenRubout = null;
        /// <summary>

        /// Protected variable to hold the drawing surface

        /// or canvas.

        /// </summary>

        protected PictureBox pictureBox = null;
        
        protected Point oldP1 = new Point(0, 0);
        protected Point oldP2 = new Point(0, 0);

        protected Point p1 = new Point(0, 0);
        protected Point p2 = new Point(0, 0);
        protected bool bDone = true;

        /// <summary>

        /// Instance variable for handling the mouse up 

        /// event for a single rubber-band object.

        /// </summary>

        protected MouseEventHandler mu = null;
        /// <summary>

        /// Instance variable for handling the mouse move 

        /// event for a single rubber-band object.

        /// </summary>

        protected MouseEventHandler mm = null;
        /// <summary>

        /// Instance variable for handling the mouse down 

        /// event for a single rubber-band object.

        /// </summary>

        protected MouseEventHandler md = null;

        /// <summary>

        /// PictureBox is the attribute holding the 

        /// PictureBox being used in the drawing of the 

        /// rubber-band object.  

        /// The PictureBox attribute is write only.

        /// </summary>

        public PictureBox PictureBox
        {
            // No getter

            set { pictureBox = value; }
        }

        protected Graphics pg1 = null;

        [System.Runtime.InteropServices.DllImportAttribute(
            "gdi32.dll")]
        private static extern bool BitBlt(
            IntPtr hdcDest, // handle to destination DC

            int nXDest, // x-coord of dest upper-left corner

            int nYDest, // y-coord of dest upper-left corner

            int nWidth, // width of destination rectangle

            int nHeight, // height of destination rectangle

            IntPtr hdcSrc,  // handle to source DC

            int nXSrc, // x-coord of source upper-left corner

            int nYSrc, // y-coord of source upper-left corner

            System.Int32 dwRop // raster operation code

            );

        /// <summary>

        /// Handles the MouseDown event for the underlying 

        /// PictureBox that is used for drawing

        /// rubber band lines (and other shapes).  

        /// A snapshot of the client area of the PictureBox 

        /// is taken, to be used when undrawing lines.

        /// </summary>

        protected void pictureBox_MouseDown(object sender, 
            System.Windows.Forms.MouseEventArgs e)
        {
            Debug.WriteLine("TSRubberBandObject - " + 
                "CreateGraphics in MouseDown");

            // Get the client rectangle for use in sizing 

            // the image and the PictureBox

            Rectangle r = pictureBox.ClientRectangle;
            // Debug.WriteLine("1 pictureBox." + 

            //    "ClientRectangle: " + r.ToString());


            // Translate the top-left point of the 

            // PictureBox from client coordinates of the

            // PictureBox to client coordinates of the 

            // parent of the 

            // PictureBox.

            Point tl = new Point(r.Left, r.Top);
            tl = pictureBox.PointToScreen(tl);
            tl = pictureBox.Parent.PointToClient(tl);
            // pictureBox.SetBounds(tl.X, tl.Y, r.Width, 

            //    r.Height, BoundsSpecified.All);


            // Get a Graphics object for the PictureBox, 

            // and create a bitmap to contain the current 

            // contents of the PictureBox.

            Graphics g1 = pictureBox.CreateGraphics();
            Image i = new Bitmap(r.Width, r.Height, g1);
			
            // Reset the Bounds of the PictureBox

            // There seems to be a bug in the Bounds 

            // attribute in the PictureBox control.

            // The top-left point must be set to the 

            // top-left client point translated to screen 

            // then back to the parents client space, and 

            // (here's the weird part) the width and height 

            // must be increased by the offset from the 

            // origin, or the image will be clipped to the 

            // original PictureBox area.

            pictureBox.Bounds = new Rectangle(tl.X, tl.Y, 
                r.Width + tl.X, r.Height + tl.Y);

            // Now assign the new image area we just created.  

            // This will be the screen area that we capture 

            // via BitBlt for our rubout pen.

            pictureBox.Image = i;

            // Reset the bounds once again, although this

            // time we can do it as it seems you should.

            pictureBox.SetBounds(tl.X, tl.Y, r.Width, 
                r.Height, BoundsSpecified.All);

            // Get a Graphics object for the image

            Graphics g2 = Graphics.FromImage(i);

            // Now get handles to device contexts, and 

            // perform the bit blitting operation.

            IntPtr dc1 = g1.GetHdc();
            IntPtr dc2 = g2.GetHdc();
            BitBlt(dc2, 0, 0, r.Width, r.Height, 
                dc1, 0, 0, 0x00CC0020 /* dest = source*/);

            // Clean up !!

            g1.ReleaseHdc(dc1);
            g2.ReleaseHdc(dc2);

            p1 = new Point(e.X, e.Y);
            bDone = false;
        }

        private void DrawXorLine(Graphics g, Point p1, 
            Point p2, Boolean bRubOut)
        {
            // Debug.WriteLine("DrawXorLine");


            if (bDone)
            {
                return;
            }

            if (pictureBox == null)
            {
                throw new NullReferenceException(
                    "RubberBandLine.pictureBox is null.");
            }

            g.CompositingMode = CompositingMode.SourceOver;

            if (bRubOut && staticTextureBrush == null &
                    & staticPenRubout == null)
            {
                if (pictureBox.Image != null)
                {
                    staticTextureBrush = new TextureBrush(
                        pictureBox.Image);
                    staticPenRubout = new Pen(
                        staticTextureBrush, this.Width + 2);
                    // Debug.WriteLine(

                    //    "Creating staticPenRubout");

                }
                else
                {
                    g.Dispose();
                    Debug.WriteLine(
                        "Cannot create staticPenRubout");
                }
            }

            if (bRubOut && staticPenRubout != null && 
                !(p1 == p2))
            {
                g.DrawLine(staticPenRubout, p1, p2);
                // Debug.WriteLine("Erase line");

            }
            else
            {
                Pen p = new Pen(this.Color, this.Width);
                p.DashStyle = this.DashStyle;
                
                g.DrawLine(p, p1, p2);
                // Debug.WriteLine("Draw line");

            }
        }

        /// <summary>

        /// Handles the MouseMove event for the underlying 

        /// PictureBox that is used for drawing rubber band 

        /// lines (and other shapes).  

        /// </summary>

        private void pictureBox_MouseMove(object sender,     
            System.Windows.Forms.MouseEventArgs e)
        {
            if (bDone)
            {
                return;
            }

            if (pg1 == null)
            {
                pg1 = pictureBox.CreateGraphics();
            }

            if (pictureBox.DisplayRectangle.Contains(e.X, 
                e.Y))
            {
                p2 = new Point(e.X, e.Y);

                // We are going to be drawing a new line,

                // so draw over the area we are about to 

                // impact with the cached background image 

                // taken before the first line is drawn.

                if (oldP1 == oldP2)
                {
                    DrawXorLine(pg1, p1, p2, false);
                }
                else
                {
                    // Debug.WriteLine("Undrawing line");

                    DrawXorLine(pg1, oldP1, oldP2, true);
                }
                
                // Debug.WriteLine("Drawing line");

                DrawXorLine(pg1, p1, p2, false);
                oldP1 = p1;
                oldP2 = p2;
            }
        }

        /// <summary>

        /// Handles the MouseUp event for the underlying 

        /// PictureBox that is used for drawing rubber band

        /// lines (and other shapes).  

        /// </summary>

        protected void pictureBox_MouseUp(object sender, 
            System.Windows.Forms.MouseEventArgs e)
        {
            p2 = new Point(e.X, e.Y);

            oldP1 = oldP2;
            bDone = true;

            staticTextureBrush = null;
            staticPenRubout = null;
        }
    }
}

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here