SVG Artiste - SVG 编辑器






4.69/5 (23投票s)
用于创建和编辑 SVG 图像的基于矢量的工具。
引言
SVG Artiste 是一个基于 SVG 的矢量编辑器。

它是对 Alexandr Shokhin 的 SVGPaint 的改进。我也非常乐意将功劳归于 Cristinel Mazarine 和 Bill Seddon 提供的 DockToolbar 和 Ruler 控件,它们为 SVG Artiste 增添了各自的功能。
背景
在我的一项商业项目中,我需要绘制一个矢量图像,它以某种方式代表我的数据,并允许用户操作矢量图像,然后相应地更新数据。那是一个 WinForms 应用程序,所以我没有选择使用 XAML(也没有数据模板 :))。于是我在 Google 上搜索,发现了 SVG Paint。我花了相当多的精力来学习项目、进行定制和实现。如我所说,需要一些级别的定制来满足我的需求,并且我将这些定制应用到了我在此处上传的项目中。
我面临的挑战是如何根据用户操作或数据为对象带来交互性,以及根据 UI 设计反馈频繁地操纵图形。我花了大量的时间和精力来完善我使用的方法,但我应该说,最终结果非常令人满意。
我的项目完成了,并且我想完善过程中使用的工具和技术。但现在我已经转向 WPF,我会说我的创造力在积极的方面获得了更强大的平台。我不得不重写 SVG Artiste 以使其渲染为 XAML 而不是 WinForms GDI。但我认为在此分享这个想法是值得的,因为任何矢量编辑器都会面临同样的挑战。
概念
矢量图形 - 它是任何视觉呈现中不可或缺的一部分,它们会被不可预测地缩放。光栅图像会失去质量,而矢量图像不会。每个矢量由其坐标表示,每个图形是由基本矢量元素组成的组。矢量元素的例子可以是圆形、矩形等。
SVG - 它是 Adobe 推出的一个标准。现在微软也加入了社区,这表明了它的强大。SVG 是 XML 的一个扩展,所以它必然是平台独立的。<rectangle/>
在 Linux、Windows 或 Mac 上都是 <rectangle/>
。SVG Artiste 2.0 使用 SVG 作为其创建的图形的序列化和传输方法。
编辑器 - 一个典型的图形编辑器会有一个工具栏、一个绘图区域以及其他帮助在屏幕上编辑图形的视觉对象。所以对于矢量图形,必须有帮助在绘图区域编辑每个元素的工具。
应用程序中的矢量图形 - 当涉及到在应用程序中呈现矢量图形时,将其作为绘图显示在一个能够读取(可能还有写入)它们的控件中,并将它们转换为应用程序能够理解的形式,并在显示系统上表示为像素点。
SVG Artiste 2.0
好了,我们有了足够的背景信息。我们将继续讨论 SVG Artiste 2.0 的工程实现(SVG Artiste 1.0 是一个基本工具,与 SVG Paint 的差异非常小)。所以,基本上,这里的外壳是一个 SDI 窗体,尽管应用程序同时支持多个图形文档。这里是实现方式。下面的图表应该能更好地解释这一点(它是用 SVG Artiste 2.0 创建的)。

外壳内部有一个可停靠的工具栏。可停靠的工具栏包含:
- 工具栏 - 所有工具都驻留在此。
- 属性 - 屏幕上选定 SVG 元素的属性。
- 工作区 - 图形创建和编辑发生在此处。
- 工作空间 - 驻留在工作区内部并保存每个绘图。工作区基本上是工作空间的集合。每个工作空间都可以独立编辑和保存。
- 控件盒 - 帮助用户自定义工作空间的属性。
这些部件中的每一个都加载到主窗体的可停靠工具栏中。这个过程将在本文稍后详细解释。
主应用程序将依赖于以下项目来完成其工作:
- 可停靠工具栏 - 管理屏幕上的布局。
- 绘图 - 管理绘图对象或图形元素的功能。
- SVGLib - 负责序列化和反序列化为 SVG。
这些都是更复杂的话题,但您可以从各自的作者那里获得详细信息。
附注:我为我的需求进行了一些定制。
截至本文撰写时,支持的重要功能列于此处:
- 创建/修改矩形、线条、椭圆、多边形和路径
- 缩放/缩小
- 多重撤销/重做
- 元素的剪切/复制/粘贴
引入核心组件
我将在下面简要解释上面提到的每个核心组件。
1. 工具栏
工具栏为用户提供选择工具的选项。没有花哨之处。在实现方面,所有工具都继承自基类 Tool
。
/// <summary>
/// Base class for all drawing tools
/// </summary>
public abstract class Tool
{
#region Fields
/// <summary>
/// If false the tool is not yet completed
/// </summary>
public Boolean IsComplete;
#endregion Fields
#region Methods
/// <summary>
/// Left mouse button is pressed
/// </summary>
/// <param name="drawArea"></param>
/// <param name="e"></param>
public virtual void OnMouseDown(DrawArea drawArea, MouseEventArgs e)
{
}
/// <summary>
/// Mouse is moved, left mouse button is pressed or none button is pressed
/// </summary>
/// <param name="drawArea"></param>
/// <param name="e"></param>
public virtual void OnMouseMove(DrawArea drawArea, MouseEventArgs e)
{
}
/// <summary>
/// Left mouse button is released
/// </summary>
/// <param name="drawArea"></param>
/// <param name="e"></param>
public virtual void OnMouseUp(DrawArea drawArea, MouseEventArgs e)
{
}
public virtual void ToolActionCompleted()
{
}
#endregion Methods
}
派生类理应实现所提供的功能。例如,如果我们处理的是矩形工具,鼠标按下应标记矩形的起始点,鼠标移动应跟踪矩形中间阶段的重调大小,鼠标抬起应根据用户选择创建具有起始点和结束点的最终矩形。您可以参考任何工具的实现来获得完整的感受。它确实是一个非常简单的实现,相信我。一旦您掌握了窍门,添加一个新工具就易如反掌。
2. 属性
我可能过于简化了手头的工作,使用了 .NET 框架提供的属性网格。由于屏幕上的每个形状都是面向对象编程中的典型对象,因此只需将它们分配给属性网格即可完成工作。如果选择了多个项目,则只显示公共属性。SVG Artiste 也遵循同样的原则。另一种方法是为每个图形元素使用自定义属性控件,这将为应用程序提供更好的直观性,但当然,这需要更多的努力。
3. 工作区
工作区是保存工作空间的区域。工作区基本上是可停靠工具栏套件提供的标签视图。我们将向工作区添加工作空间。
4. 工作空间
这里是进行绘图的地方。我在这里采用了两步法。工作空间驻留在 WorkSpaceHolder 中(两者都是用户控件)。我遵循以下方法有两个简单原因:
- 允许滚动
- 添加标尺控件
工作空间包含 DrawArea
控件,其工作是:
- 在屏幕上渲染形状
- 接收用户输入(取决于所选工具)
- 保持图形的当前状态(我指的是图形列表)
5. 控件盒
这里可以控制工作空间的各种方面。目前有三个。
- 缩放
- Grid
- SVG 文档的高度/宽度和描述
内部机制
既然我们有了配方,那就来烹饪 SVG Artiste 吧。我将遵循下面提到的用例来更好地解释 SVG Artiste 的内部机制:
- 用户启动 SVG Artiste。
- 用户打开一个 SVG 文件,SVG Artiste 会在屏幕上加载它。
- 用户创建一个新文件,绘制一个矩形,进行一些基本操作,然后保存它。
第一个用例
好的。让我们从第一个用例开始。这部分将涵盖应用程序如何利用可停靠的工具栏向用户呈现自身,以及其他组件如何在主外壳上呈现。作为一名 .NET 开发人员,我倾向于构想一个像 Visual Studio 这样的界面。所以,在众多选择中,我决定使用 Cristinel Mazarine 的 Dockable Windows Toolkit,这会给我的应用程序一个熟悉的外观和感觉,这可能并不令人惊讶。主 SDI 应用程序包含一个 DockContainer
实例。这里的过程就像向窗口添加任何其他控件一样。然后我创建并添加了前面解释的每个核心组件。下面的代码片段解释了这个过程:
//Work are is created here
_svgMainFiles = new WorkArea();
_svgMainFiles.PageChanged += OnPageSelectionChanged;
_svgMainFiles.ToolDone += OnToolDone;
_svgMainFiles.ItemsSelected += SvgMainFilesItemsSelected;
//The toolbar is added here
_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;
//Workspace control box is created here
_svgProperties = new WorkSpaceControlBox();
_svgProperties.ZoomChange += OnZoomChanged;
_svgProperties.GridOptionChange += GridOptionChaged;
_svgProperties.WorkAreaOptionChange += SvgPropertiesWorkAreaOptionChange;
//The Workarea is created here
_infoFilesMain = _docker.Add(_svgMainFiles, zAllowedDock.Fill,
new Guid("a6402b80-2ebd-4fd3-8930-024a6201d001"));
_infoFilesMain.ShowCloseButton = false;
//Document properties are added here
_infoDocumentProperties = _docker.Add(_svgProperties, zAllowedDock.All,
new Guid("a6402b80-2ebd-4fd3-8930-024a6201d003"));
_infoDocumentProperties.ShowCloseButton = false;
//The properties of shapes are created here
_shapeProperties = new shapeProperties();
_shapeProperties.PropertyChanged += ShapePropertiesPropertyChanged;
_infoShapeProperties = _docker.Add(_shapeProperties, zAllowedDock.All,
new Guid("a6402b80-2ebd-4fd3-8930-024a6201d004"));
_infoShapeProperties.ShowCloseButton = false;
很好,我们已经创建了所有部分。让我们将它们全部放在主外壳的 الرئيسي docker 控件上。下面引用的 SvgMainShown
函数中的代码片段解释了这一点。我决定在窗体完全加载后执行此操作,因此选择了 shown 事件。
_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);
我希望在此部分阐述几点:
- GUID 是用于标识添加到 docker 的每个窗体的唯一数字。这是可停靠工具栏使用的约定,我只是采用了它。对我来说效果很好,所以我没有过多抱怨。
- 我关闭了所有关闭按钮,以避免重新排列它们的复杂性。但也许以后我可以启用它,增加额外的灵活性。
- 窗口是完全可重新排列的,就像在 Visual Studio 中一样。只需单击您想要移动的工具栏的标题栏,并将其放置在拖动过程中显示的视觉提示中。用户可以根据自己的创造力将工具箱放置在任何他们想要的位置。下面的截图展示了几种排列工具箱的方式。一旦添加了更多工具箱,这将非常有用。使用可停靠工具包,排列屏幕窗口的灵活性非常棒。
- 我使用 .NET 中的基本事件机制来进行应用程序中各个窗体之间的通信。我应该说,SVG Artiste 应用程序的核心是绘图区域,它是绘图方面的“发生地”。这就是我如何做到的:
第二个用例
现在我们有了框架,让我们继续第二个用例。为了简化,我移除了 SVG Paint 用于处理文档管理的框架 Doc Toolkit。稍后,我将添加它,它不仅会添加最近列表等附加功能,还会保持应用程序的结构。
回到 SVG Artiste 的实现,DrawArea
能够处理文件操作。LoadFromXml
函数完成了这项工作。
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;
}
DrawArea
内部使用 SVGLib 来读取和写入其文件。所以我会简要介绍一下 SVGLib。
SVGLib 基本上是一个框架,它可以读取 SVG 文件,解析文档中的形状,识别其参数,并将它们转换为 SVG 元素。SVG 元素是基本的 SVG 形状,例如椭圆。SVGLib 有一个支持的形状列表,如果您想添加更多,只需扩展任何基本形状即可。另一个值得提及的术语是 SVG 属性。它可以被认为是 SVG 元素的属性,每个元素都关联到一个属性列表。所以基本上,对于 SVG Artiste,我们需要知道的是,SVGLib 将 SVG 文档转换为 SVG 元素的列表,其中每个元素都与一组属性相关联。
现在回到 DrawArea
,可以将其视为 SVG-GDI 桥梁,这意味着,要在屏幕上渲染 SVG 元素,我们需要先将其转换为 graphics.rectanlgle()
或 graphics.ellipse()
。DrawArea
内部有一个 ArrayList
,其中包含 DrawObject
列表。每个 DrawObject
都是通过迭代 SVGLib 的内部元素列表并将其转换为 DrawObject
来创建的。LoadFromXml
函数就是这样做的。
转换如何发生的详细信息可以通过检查 GraphicsList
类中的 AddFromSvg
函数来了解。
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
处理 DrawObject
的创建,从而完成整个循环。
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;
}
上面的代码片段显示了如何基于 SVGLib
对象的元素类型创建对象(可以看到应用工厂模式的机会)。
总而言之,流程如下:
DrawArea
使用SVGLib
加载 SVG 文件。SVGLib
解析文件并将其转换为SVGElement
列表。GraphicsList
使用SVGLib
的列表来填充其内部的Graphics
对象列表。DrawArea
使用GraphicsList
在屏幕上渲染每个DrawObject
。
在此背景下,有几点值得一提:
- 每个形状都继承自
DrawObject
,而DrawObject
有一个Draw
方法。因此,可以预见,DrawArea
的渲染方法会迭代GraphicsList
并调用Draw
方法来完成其工作。 - 对 SVG 操作的任何通用操作都涉及迭代
GraphicsList
并操纵其中的DrawObject
。
第三个用例
本节的目的是解释在形状上执行的基本操作,或者更准确地说,SVG Artiste 如何处理它们。当应用程序打开时,默认会显示一个新的空白文档。如果用户想创建一个新的,他可以单击工具栏上的新建文档图标或使用菜单选项。
现在新文档已打开,用户从工具箱中选择一个工具。当选择一个工具时,工具箱会引发一个 ToolSelectionChanged
事件。这由主外壳处理,主外壳再将其传播到活动文档的 DrawArea
。然后,DrawArea
的 ActiveTool
被设置为用户选择的任何工具。现在,将要在 DrawArea
上发生的事件将由 ActiveTool
处理。
例如,假设用户选择了一个矩形工具。DrawArea
中的 ActiveTool
被设置为 Rectangle
。然后用户绘制一个矩形,这是一个鼠标按下-鼠标移动-鼠标抬起的过程。基本上,SVG Artiste 使用的方法是在鼠标按下时创建一个对象,然后根据鼠标移动进行操作,并在鼠标抬起时完成活动。
现在对象已创建,GraphicsList
将包含一个 Rectangle
对象。DrawArea
中的 Render
函数将调用 Rectangle
对象的 Draw
方法,该方法会在屏幕上渲染形状。此外,选定的对象会被设置为 PropertyGrid
,以便显示其属性。更改属性会更改对象的属性,重新绘制会用新属性渲染形状。
如果我在这里犯了一个错误怎么办?我想创建一个椭圆而不是矩形。我想删除它。因此,删除是创建的相反操作。换句话说,删除是创建的撤销。命令模式通常用于这些场景,SVG Artiste 也是如此。ICommand
接口有两个方法:Execute
和 UnExecute
。
namespace Draw.Command
{
public interface ICommand
{
void Execute();
void UnExecute();
}
}
为了说明该功能,让我们以 CreateCommand
为例。
class CreateCommand : ICommand
{
private readonly ArrayList _graphicsList;
private readonly DrawObject _shape;
public CreateCommand(DrawObject shape, ArrayList graphicsList)
{
_shape = shape;
_graphicsList = graphicsList;
}
//Disable default constructor
private CreateCommand()
{
}
public void Execute()
{
_graphicsList.Insert(0, _shape);
}
public void UnExecute()
{
_graphicsList.Remove(_shape);
}
}
代码不言自明。Execute
添加一个形状,Unexecute
删除它。每当执行一个命令时,它都会被推入一个堆栈。如果用户决定撤销,SVG Artiste 会弹出最后一个命令并调用其 Unexecute
方法,然后将其推入重做堆栈。这就是撤销-重做机制。
现在,当用户完成编辑后,他/她决定保存。我们有一个 GraphicsList
中的 DrawObject
列表。现在我们不去研究 SVGLib,因为我们需要将 DrawObject
转换为 SVGElement
,然后重新创建结构并保存。为了简化,每个 DrawObject
都可以将自己转换为 SVG 字符串。所以我们将迭代 GraphicsList
,将每个 DrawObject
转换为字符串,然后将其写入文件。
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;
}
将头部附加到 sXml
,我们就得到了最终的 SVG 图像文档。
关注点
我想在此展示的还有如何基于其他贡献者的零散贡献来构建一个功能性应用程序。我真的很钦佩他们的技能,这只是通过利用他们的工作来认可他们的一种方式。我会说 XAML 非常类似于 SVG。而 SVG 先于 XAML 出现。它是一个非常强大但未被充分利用的标准。我认为围绕它赋予其强大功能的工具并不多。基本上,如果围绕 SVG 有足够的工具,用 XAML 可以做到的事情都可以用 SVG 实现。有一些很好的工具利用 SVG,比如 InkScape。但是与开发环境的集成仍然不足。SVG Artiste 还有很多可以改进的地方。我希望社区能通过宝贵的建议、代码贡献等方式帮助我。
Possible Enhancements
- 每个工具提供更多选项
- 键盘快捷键
- 组件之间更好的通信(目前是通过事件和直接函数调用)
- 日志记录
- 基于插件的架构
- 多点触控支持
- 手势识别
- [列表并非详尽无遗]
历史
- 2010 年 7 月 31 日:文章更新
- 迁移到 Visual Studio Express 2010
- 铅笔工具复制的错误修复