
Introduction
Hello. This is my 3rd article, after this one and this one. I propose a simple 2D vector graphic editor. It manages simple graphic objects like rectangles, lines and ellipses, other than images and RTF text. It shows most of the capabilities of GDI+, such as color transparency, pen style, start/end line cap and so on. All of the images shown in this article have been realized with the tool I propose.
Using the Code

VectShape
is the UserControl
used to show and manipulate the objects. It contains the mouse event logic and the "extended" double buffer logic for rendering. It exposes a big set of methods. These methods are used mainly by toolBox
, which is the control used to manipulate the shapes. Shapes
is a collection of Ele
. Ele
is the base class for all of the objects: rectangles
, boxText
, Lines
, Ellipses
and imageBoxes
. RichForm2
is the form used to edit RTF text. It's a simple form with a rich text box and a tool strip. Go to here for a better example of an RTF editor.
GDI+
When searching The Code Project for GDI+, you'll find many good articles that explain what GDI+ is and the basis of drawing in .NET. In this article, I do not address these basic problems. An in-depth article on GDI+ can be found here.
Add Controls to Your Own Form
Simply put the controls vectShaper
and toolBox
into your form. Then link toolBox
to vectShaper
in the form (i.e. Form1
) constructor:
public Form1()
{
InitializeComponent();
this.toolBox1.setVectShape(this.vectShapes1);
}
Now you're ready to start drawing some shapes.
Points of Interest
Drawing Objects: Extending Double Buffer
You can find many in-depth articles about Double Buffering (DB) on The Code Project. With the following 2 images, I will demonstrate how double buffering works. The idea of DB is to make the drawing on a memory bitmap buffer and then draw the buffer over the control.

In this project, I extended the double buffer with two memory buffers. One is for drawing static objects and one is for storing dynamic/moving objects. See the next image:

backBuffG=Graphics.fromimage(backBuff)
draws static objects over backBuffG
buffG=Graphics.fromimage(buff)
draws bitmap backBuffG over buffG
draws moving objects over buffG
ctrG=ctr.CreateGraphics()
draws bitmap buffG over ctrG
backBuff
is declared outside of the control redraw method. By STATIC objects, I mean all objects that we know aren't going to be moved/added/deleted/updated. In this kind of application, all of the objects that are not selected are static objects. Steps 1 and 2 are done only when we update the static objects. So, we don't redraw static objects in the paint
method, but only the bitmap that "contains" them (Step 4).

In this example, the blue objects are static. The selected objects -- the green selection rectangle and the handle rectangle -- are all dynamic/moving objects. So when I add/remove/update an object, such as in the MouseUp
event of the control, I redraw the control including Steps 1 and 2. When I move a selected object -- i.e. in the MouseMove
event or in the OnPaint
event of the control -- I do not need to perform Steps 1 and 2. The static objects are drawn because I stored them in backBuff
.
Rendering RichTextBox (RTF) on a Graphic Object
Another interesting point in this project is the rendering of the RTF contained in a rich text box. The Graphics.DrawString
method lets you do something like that, but with a significant limitation. You can specify only one font, color or dimension in the same string. So, it does not draw truly RTF text. I was interested in rendering RTF text, so I Googled it and found this. It explains how to print the RTF contained in a rich text box control. The article contains the code for creating RichTextBoxPrintCtrl
and demonstrates how to use it:
[DllImport("USER32.dll")]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg,
IntPtr wp, IntPtr lp);
public int Draw(int charFrom, int charTo, Graphics g,
int x, int y, int x1, int y1, double conversion)
{
RECT rectToPrint;
rectToPrint.Top =(int)(y * conversion);
rectToPrint.Bottom = (int)(y1 * conversion);
rectToPrint.Left = (int)(x * conversion);
rectToPrint.Right = (int)(decimal)(x1 * conversion);
RECT rectPage;
rectPage.Top = (int)(y * conversion);
rectPage.Bottom = (int)(y1 * conversion);
rectPage.Left = (int)(x * conversion);
rectPage.Right = (int)(x1 * conversion);
IntPtr hdc = g.GetHdc();
FORMATRANGE fmtRange;
fmtRange.chrg.cpMax = charTo; fmtRange.chrg.cpMin = charFrom;
fmtRange.hdc = hdc; fmtRange.hdcTarget = hdc; fmtRange.rc = rectToPrint; fmtRange.rcPage = rectPage;
IntPtr res = IntPtr.Zero;
IntPtr wparam = IntPtr.Zero;
wparam = new IntPtr(1);
IntPtr lparam = IntPtr.Zero;
lparam = Marshal.AllocCoTaskMem(Marshal.SizeOf(fmtRange));
Marshal.StructureToPtr(fmtRange, lparam, false);
res = SendMessage(Handle, EM_FORMATRANGE, wparam, lparam);
Marshal.FreeCoTaskMem(lparam);
g.ReleaseHdc(hdc);
return res.ToInt32();
}
After that, there was a problem. The background color of the control RichetextBox
was also rendered, while I was interested rendering only the text. So I found this, which explains how to make a control transparent:
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
static extern IntPtr LoadLibrary(string lpFileName);
protected override CreateParams CreateParams
{
get
{
CreateParams prams = base.CreateParams;
if (LoadLibrary("msftedit.dll") != IntPtr.Zero)
{
prams.ExStyle |= 0x020; prams.ClassName = "RICHEDIT50W";
}
return prams;
}
}
Et voilà. At this point, we have all we need to render real RTF text on a graphic object. I called it BoxTesto
; it is a box (rectangle) with a start and an end point and all of the properties derived from the Ele
class. In addition, it has an RTF property that contains a string with the RTF text. When I draw a BoxTesto
, I create a "transparent" RichTextBoxPrintCtrl
control and assign RichTextBoxPrintCtrl.rtf
with BoxTesto.rtf
. Then I call RichTextBoxPrintCtrl.Draw
.
Undo/Redo
I choose to implement the undo/redo as a double-linked list. Each item on the list contains a reference to the object of interest, an old copy of the object and a new copy of the object. In the next image, I present the undo/redo list in blue and the modified objects in black.

Every time I change an object, I store an element in the undo/redo list. When I need to undo an action, I copy the OLD object properties over the interested object properties. Then I go back one element in the undo/redo list. When I need to redo an action, I go forward one element in the undo/redo list. Then I copy the NEW object properties over the object referenced by the element. The "lazy" way: in my old project, I implemented undo/redo like a circular array where the objects stored were the entire collection of graphic objects. That was an easy and fast solution, but too memory-hungry.
Drawing Images (Rotation/Transparency)

Now I focus on image drawing (method Draw
, class ImgBox
, file Shapes.cs) to show how to implement image rotation and transparency:
Color backColor = this.img.GetPixel(0, 0);
int dim =
(int)System.Math.Sqrt(img.Width * img.Width + img.Height * img.Height);
Bitmap curBitmap = new Bitmap(dim, dim);
Graphics curG = Graphics.FromImage(curBitmap);
if (this.Rotation >
0)
{
Matrix X = new Matrix();
X.RotateAt(this.Rotation, new PointF(curBitmap.Width / 2,
curBitmap.Height / 2));
curG.Transform = X;
X.Dispose();
}
curG.DrawImage(img, (dim - img.Width) / 2, (dim - img.Height) / 2,
img.Width, img.Height);
if (this.Trasparent)
curBitmap.MakeTransparent(backColor);
curG.Save();
g.DrawImage(curBitmap, new Rectangle(this.X + dx,
this.Y + dy, this.X1 - this.X, this.Y1 - this.Y));
curG.Dispose();
curBitmap.Dispose();
History
- 15 June, 2007 -- Original version posted
Update 1
- Added
ZoomIN
/ZoomOut
- Added right mouse click canvas movement
- Added image rotation
- Added Transparent Image property
Update 2
- Added obj --> Group and obj --> deGroup functions
- Added a rotation handle for object rotation

- Added File--> Save selected objects / File --> Load objects
- Created Help page
Update 3
- Added Gradient Line Fill Color, i.e. draw a shape, fill it and then set the
UseGradientLineColor
property to true
- Added Group Resize / Rotation and Zoom Management
- Can show a Group like a
GraphicPath
, i.e. select some Line
s and then select them, group them and set the graphPath
property to true
- Added example *.shape files. Load them from File --> Load

Update 4
- Added Free Hand Pen Tool
- Added Obj-->Poly--> X/Y Mirror
- Added Obj-->Poly--> Merge
- Added Obj-->Poly--> Extract Points

Update 5
- Added Draw Graph Tool