SVG Vector
Graphics Editor With C++ Source Code.
1
SVG
Artiste is a vector
editor based on
SVG.
2
For one of my commercial projects, I was
required to draw
a vector
image, which in a way represents the data I have
and allows users to manipulate the
vector
image, and then the data needed to be updated
accordingly. It was a WinForms application so I
didn't have the option of using XAML over there
(neither data templates :)). So then I searched
over Google and found
SVG
Paint. I had to spend considerable effort on the
curve of learning the project, then customizing
and implementing it. As I mentioned, some levels
of customization were required to fit my
requirements and I am carrying it through to the
project which is uploaded here.
The challenges I faced were to bring
interactivity to the objects based on user
actions or data, and to manipulate the graphics
frequently based on UI design feedbacks. I had
to spend a considerable amount of time and
effort on perfecting the approach I used, but I
should say that the end result was very
satisfactory.
My project got completed and I wanted to
perfect the tools and techniques used in the due
course. But now that I have shifted myself to
WPF, I would say my creativity has got a
stronger platform on the positive side. I had to
rewrite
SVG
Artiste to render as XAML instead of WinForms
GDI. But I feel it is worthwhile sharing the
idea here as the core challenges still remain
the same for any vector
editor.
3
Vector Graphics
- It is an indispensable part of any visual
presentation which will be scaled unpredictably.
Raster will lose its quality,
vectors
won't. Each vector
is represented by its co-ordinates and each
graphics is a group of basic
vector
elements. Examples of
vector elements can be circle,
rectangle etc.
SVG
- It is a standard brought forward by Adobe. Now
Microsoft is also joining the community, which
shows its strength.
SVG
is an extension of XML, so it is bound to be
platform independent. <rectangle/>
is <rectangle/>
in Linux, Windows
or Mac.
SVG
Artiste 2.0 uses
SVG
as a method of serialization and transportation
of graphics created out of it.
Editors -
A typical graphical
editor would have a toolbar, a
drawing area, and other visual objects that help
edit the graphic on screen. So for
vectors, it
has got to be tools that help edit each element
on the drawing area.
Vectors
with Apps - When it comes to presenting the
vector graphics
in an application, present it as a drawing in a
control which is capable of reading (and
probably writing) them and
converting
them to the form in which the application can
understand and represent them as pixel points on
the display system.
SVG
OK, we have had enough background now. We
will move on to how the
SVG
Artiste 2.0 is engineered (SVG
Artiste 1.0 is a basic tool with very few
variations from
SVG
Paint). So basically, the shell is an SDI form
here even though the application supports
multiple graphic documents at the same time.
Here's how it is done. The diagram below should
better explain it (it was created with
SVG
Artiste 2.0).
The shell has a dockable toolbar inside it.
The dockable toolbar has:
- Toolbar - All tools reside here.
- Properties - Properties of the selected
SVG
element on screen.
- WorkArea - Where the graphic creation
and editing takes place.
- WorkSpace - Resides inside the WorkArea
and holds each drawing. A WorkArea is
basically a collection of WorkSpaces. Each
WorkSpace can be edited and saved
independent of each other.
- Controlbox - Helps users customize the
properties of a WorkSpace.
Each of these parts are loaded on to the
dockable toolbar in the main form. The process
will be explained in detail, later in this
article.
The main application is going to depend on
the following projects for doing its job:
- Dockable toolbar - Manages the layout
onscreen.
- Draw - Manages the functionality of the
draw objects or
graphic elements.
- SVGLib
- Takes care of serialization and
deserialization to
SVG.
Each of this is a bigger topic, but you can
get the details from their respective authors.
P.S.: I have made certain
customizations for my requirement.
The important functionalities supported, as
of the time of writing this article, are listed
here:
- Creation/Modification of rectangle,
line, ellipse, polygon and path
- Zoom/UnZoom
- Multiple Undo/Redo
- Cut/Copy/Paste of elements
Introducing Core Components
Let me explain each of the above mentioned
core components briefly here.
1. Toolbar
The Toolbar provides the users a choice to
select a tool. No twists here. Coming to the
implementation, all tools are derived from the
base class Tool
.
Collapse |
Copy Code
public abstract class Tool
{
#region Fields
public Boolean IsComplete;
#endregion Fields
#region Methods
public virtual void OnMouseDown(DrawArea drawArea, MouseEventArgs e)
{
}
public virtual void OnMouseMove(DrawArea drawArea, MouseEventArgs e)
{
}
public virtual void OnMouseUp(DrawArea drawArea, MouseEventArgs e)
{
}
public virtual void ToolActionCompleted()
{
}
#endregion Methods
}
The derived classes, obviously, are expected
to implement the features presented. For
example, if we are dealing with a rectangle
tool, a mouse down should mark the starting
points of the rectangle, a mouse move should
track the resizing of the intermediary stages of
the rectangle, and a mouseup should create the
final rectangle with the starting point and
ending point as per user selection. You can
refer to any of the tools implementation to get
the full feel of it. It really is a very simple
implementation which is used here, trust me.
Adding a new tool is just a piece of cake once
you get a feel of it.
2. Properties
I have probably overly simplified the job in
hand by making use of the properties grid
provided by the .NET framework. Since each of
the shapes onscreen are typical objects as in
OOP, simply assigning them to the property grid
does the trick. If multiple items are selected,
only the common properties are shown.
SVG
Artiste also follows the same religion. Another
approach is to use a custom property control for
each graphic element, which will give better
intuitiveness to the application, but of course,
at the price of more effort.
3. WorkArea
WorkArea is a place which holds WorkSpaces. A
WorkArea is basically a tabbed view as provided
by the dockable toolbar kit. To a WorkArea, we
will add WorkSpace(s).
4. WorkSpace
This is the place where the drawings are
done. Here I have done a two step approach. The
workspace resides inside a WorkSpaceHolder (both
being user controls). I have followed the below
approach for two simple reasons:
- To allow scrolling
- To add in the ruler control
The WorkSpace holds the DrawArea
control, whose jobs are:
- Render the shapes on screen
- Take user inputs (depending on the tool
selected)
- Hold the current state of the graphic (I
am referring to the graphic list)
5. ControlBox
This is the place where the various aspects
of the WorkSpace can be controlled. Presently
there are three of them.
- Zoom
- Grid
- Height/Width and Description of
SVG
Document
4
Now that we have the recipe, let's cook the
SVG
Artiste. I will follow the below mentioned use
cases to better explain the internals of
SVG
Artise:
- User starts up
SVG
Artiste.
- User opens an
SVG
file and
SVG
Artiste loads it for him/her onscreen.
- User creates a new file and he/she draws
a rectangle, does some basic operations, and
then saves it.
The First UseCase
Okay. Let's start with the first use case.
What this portion will cover is how the
application makes use of a dockable toolbar to
present itself to the user and how other
components are presented on the main shell. I,
being a .NET developer, am bound to be a
penchant to think of an interface like Visual
Studio. So it might not be surprising that,
amongst other alternatives, I decided to make
use of the Dockable Windows Toolkit by Cristinel
Mazarine, which would give my application a
familiar look and feel. The main SDI application
holds an instance of DockContainer
.
The process here is just like adding any other
control to a window. Then I created and added
each of the core components explained earlier.
The following code snippet explains the process:
Collapse |
Copy Code
_svgMainFiles = new WorkArea();
_svgMainFiles.PageChanged += OnPageSelectionChanged;
_svgMainFiles.ToolDone += OnToolDone;
_svgMainFiles.ItemsSelected += SvgMainFilesItemsSelected;
_toolBox = new ToolBox {Size = new Size(113, 165)};
_toolBox.ToolSelectionChanged += ToolSelectionChanged;
_infoToolbar = _docker.Add(_toolBox, zAllowedDock.All,
new Guid("a6402b80-2ebd-4fd3-8930-024a6201d002"));
_infoToolbar.ShowCloseButton = false;
_svgProperties = new WorkSpaceControlBox();
_svgProperties.ZoomChange += OnZoomChanged;
_svgProperties.GridOptionChange += GridOptionChaged;
_svgProperties.WorkAreaOptionChange += SvgPropertiesWorkAreaOptionChange;
_infoFilesMain = _docker.Add(_svgMainFiles, zAllowedDock.Fill,
new Guid("a6402b80-2ebd-4fd3-8930-024a6201d001"));
_infoFilesMain.ShowCloseButton = false;
_infoDocumentProperties = _docker.Add(_svgProperties, zAllowedDock.All,
new Guid("a6402b80-2ebd-4fd3-8930-024a6201d003"));
_infoDocumentProperties.ShowCloseButton = false;
_shapeProperties = new shapeProperties();
_shapeProperties.PropertyChanged += ShapePropertiesPropertyChanged;
_infoShapeProperties = _docker.Add(_shapeProperties, zAllowedDock.All,
new Guid("a6402b80-2ebd-4fd3-8930-024a6201d004"));
_infoShapeProperties.ShowCloseButton = false;
Good, we have created all the parts. Let's
put it all together onto the main docker control
in our main shell. The below mentioned snippet
from the function
SvgMainShown
explains it. I decided to do it once the form is
loaded completely and hence the choice of the
shown event.
Collapse |
Copy Code
_docker.DockForm(_infoToolbar, DockStyle.Left, zDockMode.Inner);
_docker.DockForm(_infoFilesMain, DockStyle.Fill, zDockMode.Inner);
_docker.DockForm(_infoDocumentProperties, DockStyle.Right, zDockMode.Inner);
_docker.DockForm(_infoShapeProperties,_infoToolbar, DockStyle.Bottom, zDockMode.Outer);
A few points I would like to elaborate on
this section are:
- The GUID is a unique number used to
identify each of the forms added to the
docker. This is the convention used by the
dockable toolbar and I have simply adopted
it. It works fine for me, so I didn't
grumble too much.
- I have turned off all the close buttons
just to avoid the complexity of rearranging
them. But may be later on, I can enable
this, adding additional flexibility.
- The windows are completely re-arrangable
as in Visual Studio. Simple click the
titlebar of the toolbar you want to move
around and put it into the visual cue shown
on the course of the drag. It's simply up to
the creativity of the user to put the
toolboxes wherever he wants them to be. The
following screenshots show jut a few ways
you could arrange your toolboxes. This will
really be handy once a lot more toolboxes
are added. The flexibility in terms of
arranging the onscreen window is really cool
with the dockable toolkit.
- I used the basic event mechanism in .NET
for the communication between each of the
forms within the application. The core of
the
SVG
Artiste application, I should say, is the
draw area, which is the "happening place" in
terms of drawings. This is how I have done
it.
The Second UseCase
Now that we have a skeleton, let us move onto
the second use case. I have taken out the doc
toolkit, which was the framework which
SVG
Paint used to handle document management, just
to make it simple. Later on, I will be adding
it, which will not only add additional features
like recent list but also keep the application
structured.
Coming back to the implementation in
SVG
Artiste, the DrawArea
is capable of
handling file operations. The function
LoadFromXml
does the job.
Collapse |
Copy Code
public bool LoadFromXml(XmlTextReader reader)
{
_graphicsList.Clear();
var svg = new SvgDoc();
if (!svg.LoadFromFile(reader))
return false;
SvgRoot root = svg.GetSvgRoot();
if (root == null)
return false;
try
{
SizePicture = new SizeF(
DrawObject.ParseSize(root.Width,DrawObject.Dpi.X),
DrawObject.ParseSize(root.Height,DrawObject.Dpi.Y));
}
catch
{
}
SvgElement ele = root.getChild();
if (ele != null)
_graphicsList.AddFromSvg(ele);
return true;
}
The DrawArea
internally makes
uses of SVGLib
to read and write its files. So I will give a
brief idea about
SVGLib.
SVGLib,
basically, is a framework which can read an
SVG
file, parse out the shapes in the document,
identify its parameters, and
convert
them into
SVG
Elements. An
SVG
Element is a basic
SVG
Shape, for example, an Ellipse.
SVGLib
has a list of supported shapes, and if you wish
to add more, it can be done by simply extending
any of the primitive shapes. Another term which
is worth mentioning is the
SVG
Attribute. It can be thought of as a property of
an
SVG
Element, and each element is associated to a
list of attributes. So basically, for
SVG
Artiste, what we need to know is that the
SVGLib
converts
the
SVG
document into a list of
SVG
Elements, each of which is associated wiht a set
of attributes.
Now coming to DrawArea
, it can
be though of as a
SVG-GDI
bridge, which means, to render the
SVG
Elements on screen, we need to
convert it
to graphics.rectanlgle()
or
graphics.ellipse()
first. The
DrawArea
internally has an
ArrayList
which holds the list of
DrawObject
s. Each of the DrawObject
is created by iterating through
SVGLib's
internal element list and
converting
it into a DrawObject
. The
LoadFromXml
function does just this.
The details on how the conversion happens is
revealed by examining the AddFromSvg
function from the GraphicsList
class.
Collapse |
Copy Code
public void AddFromSvg(SvgElement ele)
{
while (ele != null)
{
DrawObject o = CreateDrawObject(ele);
if (o != null)
Add(o);
SvgElement child = ele.getChild();
while (child != null)
{
AddFromSvg(child);
child = child.getNext();
}
ele = ele.getNext();
}
}
CreateDrawObject
handles the
DrawObject
creation which completes
the cycle.
Collapse |
Copy Code
DrawObject CreateDrawObject(SvgElement svge)
{
DrawObject o = null;
switch (svge.getElementType())
{
case SvgElement._SvgElementType.typeLine:
o = DrawLine.Create((SvgLine )svge);
break;
case SvgElement._SvgElementType.typeRect:
o = DrawRectangle.Create((SvgRect )svge);
break;
case SvgElement._SvgElementType.typeEllipse:
o = DrawEllipse.Create((SvgEllipse )svge);
break;
case SvgElement._SvgElementType.typePolyline:
o = DrawPolygon.Create((SvgPolyline )svge);
break;
case SvgElement._SvgElementType.typeImage:
o = DrawImage.Create((SvgImage )svge);
break;
case SvgElement._SvgElementType.typeText:
o = DrawText.Create((SvgText )svge);
break;
case SvgElement._SvgElementType.typeGroup:
o = CreateGroup((SvgGroup )svge);
break;
case SvgElement._SvgElementType.typePath:
o = DrawPath.Create((SvgPath)svge);
break;
case SvgElement._SvgElementType.typeDesc:
Description = ((SvgDesc)svge).Value;
break;
default:
break;
}
return;
}
The above code snippet shows how the object
creation is done based on the element type of
the SVGLib
object (can see an opportunity for Factory
pattern).
To summarize, the flow is as follows:
DrawArea
makes use of
SVGLib
to load the
SVG
file
SVGLib
parses the file and
converts
it into a list of
SVGElement
s
GraphicsList
makes use of
SVGLib
's
list to populate its internal list with
Graphics
objects
DrawArea
makes use of
GraphicsList
to render each DrawObject
on
screen
A few points worth mentioning in this context
are:
- Each shape is derived from
DrawObject
, and DrawObject
has a Draw
method. So,
predictably, the rendering method of
DrawArea
iterates through the
GraphicsList
and calls the Draw
method to
get its job done
- Any generic operation on the
SVG
operation involves iterating the
GraphicsList
and manipulating the
DrawObject
s in it
4
The idea of this section is to explain the
basic operations which are performed on the
shapes, or rather how
SVG
Artiste handles it. When the application is
opened, it presents a new blank document by
default. If the user wants to make a new one, he
can click the New Document icon on the toolbar
or use the menu option.
Now that the new document is opened, the user
selects a tool from the toolbox. When a tool is
selected, the toolbox raises a
ToolSelectionChanged
event. This is
handled by the main shell, which in turn
propagates it to the DrawArea
of
the active document. Then the ActiveTool
of the DrawArea
is set to whatever
the user has selected. Now the events that are
going to happen on the DrawArea
are
going to be handled by the ActiveTool
.
Say, for example, the user has selected a
rectangle tool. The ActiveTool
is
set to Rectangle
in the
DrawArea
. The user then draws a
rectangle, which is a
MouseDown-MouseMove-MouseUp transaction.
Basically, the approach used in
SVG
Artiste is to create an object at mouse-down and
then manipulate based on the mouse-move and
complete the activity on mouse-up.
Now that the object is created, the
GraphicsList
will hold a Rectangle
object. The Render
function in
DrawArea
will call the Draw
method of the Rectangle
object,
which renders the shape on the screen. Also the
selected object(s) are set to the
PropertyGrid
so that the properties are
shown. Changing a property will change the
object's property and a redraw renders the shape
with the new property.
What if I did a mistake here? I wanted to
create an ellipse and not a rectangle. I would
want to delete it. So Delete is the opposite of
Create. Or in other words, Delete is undo for
Create. Command pattern is typically used in
these scenarios, and so did
SVG
Artiste. The ICommand
interface has
two methods: Execute
and
UnExecute
.
Collapse |
Copy Code
namespace Draw.Command
{
public interface ICommand
{
void Execute();
void UnExecute();
}
}
To illustrate the functionality, let's take
the example of CreateCommand
:
Collapse |
Copy Code
class CreateCommand : ICommand
{
private readonly ArrayList _graphicsList;
private readonly DrawObject _shape;
public CreateCommand(DrawObject shape, ArrayList graphicsList)
{
_shape = shape;
_graphicsList = graphicsList;
}
private CreateCommand()
{
}
public void Execute()
{
_graphicsList.Insert(0, _shape);
}
public void UnExecute()
{
_graphicsList.Remove(_shape);
}
}
The code is self explanatory. Execute
adds a shape, Unexecute
deletes it.
Whenever a command is executed, it is pushed to
a stack. If the user decides to undo,
SVG
Artiste pops out the last command and calls its
Unexecute
method and pushes it to a
redo stack. That's all about the undo-redo
mechanism.
Now when the user has done with editing,
he/she decides to save it. We have a list of
DrawObject
s in the
GraphicsList
. We are not going to go
through
SVGLib
now, as we have to
convert
the DrawObject
to an
SVGElement
and then recreate the structure and save. To
make it simpler, each DrawObject
can convert
itself to an SVG
string. So we will be iterating through the
GraphicsList
and
converting
each DrawObject
to a string and
then write it to a file.
Collapse |
Copy Code
public string GetXmlString(SizeF scale)
{
string sXml = "";
int n = _graphicsList.Count;
for (int i = n - 1; i >= 0; i-- )
{
sXml += ((DrawObject)_graphicsList[i]).GetXmlStr(scale);
}
return sXml;
}
Append the header to sXml
and we
have the final
SVG
image document.
5
What I would like to present here is also how
we can make a functional application based on a
group of disparate contributions from other
contributors. I really admire them for their
skills and this is just a way of acknowledging
them - by making use of their work. I would say
XAML is very similar to
SVG.
And
SVG
came first. It's a very powerful but underused
standard. I would say not much tools came around
it that empowers it. Basically, whatever can be
done with XAML could have been achieved with
SVG
if there were sufficient tools around it. There
are quite good tools which do make use of
SVG,
like InkScape. But the integration with
development environments is still lacking. There
are tons of enhancements to me made on
SVG
Artiste. I hope the community is going to help
me with it through valuable suggestions, code
contributions, and so on.