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.
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.
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);
}
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.
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
.
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.