Simple Vector Shapes






4.98/5 (108 votes)
A 2D vector shapes and RTFcontrol editor
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();
// added for linking toolbox to vectshapes
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);
// I extended the RichTextBoxPrintCtrl code by adding a Draw method:
public int Draw(int charFrom, int charTo, Graphics g,
int x, int y, int x1, int y1, double conversion)
{
//Calculate the area to render and print
RECT rectToPrint;
rectToPrint.Top =(int)(y * conversion);
rectToPrint.Bottom = (int)(y1 * conversion);
rectToPrint.Left = (int)(x * conversion);
rectToPrint.Right = (int)(decimal)(x1 * conversion);
//Calculate the size of the page
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;//Indicate character from to character to
fmtRange.chrg.cpMin = charFrom;
fmtRange.hdc = hdc; //Use the same DC for measuring & rendering
fmtRange.hdcTarget = hdc; //Point at printer hDC
fmtRange.rc = rectToPrint; //Indicate the area on page to print
fmtRange.rcPage = rectPage; //Indicate size of page
IntPtr res = IntPtr.Zero;
IntPtr wparam = IntPtr.Zero;
wparam = new IntPtr(1);
//Get the pointer to the FORMATRANGE structure in memory
IntPtr lparam = IntPtr.Zero;
lparam = Marshal.AllocCoTaskMem(Marshal.SizeOf(fmtRange));
Marshal.StructureToPtr(fmtRange, lparam, false);
//Send the rendered data for printing
res = SendMessage(Handle, EM_FORMATRANGE, wparam, lparam);
//Free the block of memory allocated
Marshal.FreeCoTaskMem(lparam);
//Release the device context handle obtained by a previous call
g.ReleaseHdc(hdc);
//Return last + 1 character printer
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:
//START : make this control trasparent
[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; // transparent
prams.ClassName = "RICHEDIT50W";
}
return prams;
}
}
// END
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:
//get the back color from the first pixel (UP-LEFT)
Color backColor = this.img.GetPixel(0, 0);
//Create a tmp Bitmap and a graphic object
// the dimension of the tmp bitmap must permit the rotation of img
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)
{
// activate the rotation on the graphic obj
Matrix X = new Matrix();
X.RotateAt(this.Rotation, new PointF(curBitmap.Width / 2,
curBitmap.Height / 2));
curG.Transform = X;
X.Dispose();
}
// I draw img over the tmp bitmap
curG.DrawImage(img, (dim - img.Width) / 2, (dim - img.Height) / 2,
img.Width, img.Height);
if (this.Trasparent)
curBitmap.MakeTransparent(backColor); // here I perform trasparency
curG.Save();
// I draw the tmp bitmap on canvas
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 totrue
- Added Group Resize / Rotation and Zoom Management
- Can show a Group like a
GraphicPath
, i.e. select someLine
s and then select them, group them and set thegraphPath
property totrue
- 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