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

C# Rubber Rectangle

0.00/5 (No votes)
30 Jun 2008 1  
Implements a Rubber Rectangle in C#.

Introduction

I've been experimenting recently with the GDI+ draw methods to make a Plot control in C#. The goal was to build a Plotter that can display multiple graphs, fit to screen, zoom in/out, pan, etc. It had to be extremely easy to incorporate into existing projects, and was actually created to totally replace a similar legacy version of a Plotter that was written in LISP.

One of the largest problems I ran into was how to create a rubber rectangle. There are some ideas floating around on the Internet, there are a lot of hacks, and I finally decided upon a method that I think works well for me. Unfortunately, those of you using Mono won't get this working as easily as I did, but for the majority of you who use Windows, you should have no problems at all.

The Problem

Imagine a screen with lots of color data on it. You want to non-destructively draw the outline of a re-sizable box. Your first thought may be to draw a box with no fill color, with a 1px width black line for a border. However, when you re-size that box, you will find that you leave a big trail of black lines behind. You could then try and erase the black lines with a white line, but that will just leave white lines behind, still effectively destroying your drawings.

whatamess.GIF

The Solution

Draw a line with a XOR pen, which will exclusive-or (XOR) all the pixel color information. So, white will turn black, black will turn white, etc. Then, draw back over your box with a second XOR pen to return your drawing to its previous state. This is non-destructive drawing! Unfortunately, there is no method (that I know of) in GDI+ to create a XOR pen.

My solution to this lack of XOR pen was to use the gdi32.dll, and then call the GDI methods from C#. Here's how I did it.

The GDI32 Class

I first created a new class that I called GDI32. Then, I used Interop to get the external methods.

[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern bool Ellipse(IntPtr hdc, int x1, int y1, int x2, int y2);
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern bool Rectangle(IntPtr hdc, int X1, int Y1, int X2, int Y2);
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern IntPtr MoveToEx(IntPtr hdc, int x, int y, IntPtr lpPoint);
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern bool LineTo(IntPtr hdc, int x, int y);
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern IntPtr CreatePen(PenStyles enPenStyle, int nWidth, int crColor);
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern IntPtr CreateSolidBrush(BrushStyles enBrushStyle, int crColor);
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern bool DeleteObject(IntPtr hObject);
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern IntPtr SelectObject(IntPtr hdc, IntPtr hObject);
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern IntPtr GetStockObject(int brStyle);
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern int SetROP2(IntPtr hdc, int enDrawMode);

The only ones you really need here are SetROP2, SelectObject, DeleteObject, CreatePen, Rectangle, and CreateSolidBrush.

Then, I created a method that can initialize the pen and brush, and a method to dispose of the pen and brush after they've been used. One thing to note, R2_XORPEN is the XOR pen. You can pass (int)7 as an equivalent if you will just be using a XOR pen.

/// <summary>
/// Initializes the pen and brush objects. Stores the old pen
/// and brush so they can be recovered later.
/// </summary>
protected void InitPenAndBrush(Graphics g)
{
    hdc = g.GetHdc();
    gdiPen = CreatePen(penStyle, lineWidth, GetRGBFromColor(PenColor));
    gdiBrush = CreateSolidBrush(brushStyle, GetRGBFromColor(fillColor));
    if (PenColor == Color.Transparent) SetROP2(hdc, (int)RasterOps.R2_XORPEN);
    oldPen = SelectObject(hdc, gdiPen);
    oldBrush = SelectObject(hdc, gdiBrush);
}

/// <summary>
/// Reloads the old pen and brush.
/// Deletes the pen that was created by InitPenAndBrush(g).
/// Releases the handle to the device context
/// and then disposes of the Graphics object.
/// </summary>
protected void Dispose(Graphics g)
{
    SelectObject(hdc, oldBrush);
    SelectObject(hdc, oldPen);
    DeleteObject(gdiPen);
    DeleteObject(gdiBrush);
    g.ReleaseHdc(hdc);
    g.Dispose();
}

Finally, I built up methods for drawing objects such as lines, rectangles, ellipses, etc.

/// <summary>
/// Draws a rectangle with the pen and brush
/// that have been set by the user. Uses gdi32->Rectangle
/// </summary>
/// <param name="g">Graphics object. You can use CreateGraphics().</param>
/// <param name="p1">First corner of rectangle.</param>
/// <param name="p2">Second corner of rectangle.</param>
public void DrawRectangle(Graphics g, Point p1, Point p2)
{
    InitPenAndBrush(g);
    Rectangle(hdc, p1.X, p1.Y, p2.X, p2.Y);
    Dispose(g);
}

Using the Code

To use the code, just include it in your project and create a new GDI32 object.

GDI32 gdi = new GDI32();

Then, you can use any of the public methods of the GDI32 class. All of them are documented, and are pretty easy to extend.

gdi.DrawRectangle(CreateGraphics(), mouseDown, new Point(e.X, e.Y)); 
gdi.DrawRectangle(CreateGraphics(), _mouseDown, oldMouse); 
oldMouse = new Point(e.X, e.Y);

The above code would go in OnMouseMove(MouseEventArgs e), and will draw a black rectangle from points mouseDown to the new point provided by the MouseEventArgs.

nomoretrails.GIF

Points of Interest

Nothing super interesting or hardcore is going on here... I just found it to be an annoying issue, with many hacks available on the internet. I think this is a pretty clean way to do it, although it does force you to stray from GDI+.

A Rubber Rectangle

Here's what you need to build your own rubber rectangle with this class. You need to override the OnMouseDown, OnMouseMove, and OnMouseUp methods. When the mouse is first pressed, you need to store the original mouse position.

protected override void OnMouseDown(MouseEventArgs e)
{
    base.OnMouseDown(e);
    if (e.Button == MouseButtons.Left)
        oldMouse = mouseDownAt = new Point(e.X, e.Y);
}

Then, you need to draw two rectangles. One that draws the visible border, and one that erases the previous border. This happens in OnMouseMove.

protected override void OnMouseMove(MouseEventArgs e)
{
    base.OnMouseMove(e);
    if (e.Button == MouseButtons.Left)
    {
        gdi.DrawRectangle(CreateGraphics(), mouseDownAt, new Point(e.X, e.Y));
        gdi.DrawRectangle(CreateGraphics(), mouseDownAt, oldMouse);
        oldMouse = new Point(e.X, e.Y);
    } 
}

Finally, you need to erase the last rectangle when the mouse is lifted.

protected override void OnMouseUp(MouseEventArgs e)
{
    base.OnMouseUp(e);
    if (e.Button == MouseButtons.Left)
    {
        gdi.DrawRectangle(CreateGraphics(), mouseDownAt, oldMouse);
    }
}

History

  • June 30, 2008 - First post.
  • June 30, 2008 - Added a demo project.

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