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.
protected System.Windows.Forms.MouseEventHandler mu = null;
protected System.Windows.Forms.MouseEventHandler mm = null;
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 );
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
{
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;
}
private int width = 1;
public int Width
{
get { return width; }
set { width = value; }
}
private Color color = Color.FromArgb(255, 0, 0, 0);
public Color Color
{
get { return color; }
set { color = value; }
}
private DashStyle dashStyle = DashStyle.Dot;
public DashStyle DashStyle
{
get { return dashStyle; }
set { dashStyle = value; }
}
protected TextureBrush staticTextureBrush = null;
protected Pen staticPenRubout = null;
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;
protected MouseEventHandler mu = null;
protected MouseEventHandler mm = null;
protected MouseEventHandler md = null;
public PictureBox PictureBox
{
set { pictureBox = value; }
}
protected Graphics pg1 = null;
[System.Runtime.InteropServices.DllImportAttribute(
"gdi32.dll")]
private static extern bool BitBlt(
IntPtr hdcDest,
int nXDest,
int nYDest,
int nWidth,
int nHeight,
IntPtr hdcSrc,
int nXSrc,
int nYSrc,
System.Int32 dwRop
);
protected void pictureBox_MouseDown(object sender,
System.Windows.Forms.MouseEventArgs e)
{
Debug.WriteLine("TSRubberBandObject - " +
"CreateGraphics in MouseDown");
Rectangle r = pictureBox.ClientRectangle;
Point tl = new Point(r.Left, r.Top);
tl = pictureBox.PointToScreen(tl);
tl = pictureBox.Parent.PointToClient(tl);
Graphics g1 = pictureBox.CreateGraphics();
Image i = new Bitmap(r.Width, r.Height, g1);
pictureBox.Bounds = new Rectangle(tl.X, tl.Y,
r.Width + tl.X, r.Height + tl.Y);
pictureBox.Image = i;
pictureBox.SetBounds(tl.X, tl.Y, r.Width,
r.Height, BoundsSpecified.All);
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 );
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)
{
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);
}
else
{
g.Dispose();
Debug.WriteLine(
"Cannot create staticPenRubout");
}
}
if (bRubOut && staticPenRubout != null &&
!(p1 == p2))
{
g.DrawLine(staticPenRubout, p1, p2);
}
else
{
Pen p = new Pen(this.Color, this.Width);
p.DashStyle = this.DashStyle;
g.DrawLine(p, p1, p2);
}
}
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);
if (oldP1 == oldP2)
{
DrawXorLine(pg1, p1, p2, false);
}
else
{
DrawXorLine(pg1, oldP1, oldP2, true);
}
DrawXorLine(pg1, p1, p2, false);
oldP1 = p1;
oldP2 = p2;
}
}
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;
}
}
}