对 DrawTools 的扩展






4.83/5 (51投票s)
DrawTools 库扩展,包含图层、缩放、平移、旋转功能
更新于 2011/10/4

引言
Alex Fr 在他的 DrawTools 文章中提供了一套出色的绘图工具,这些工具构成了本文的基础。本文在原有的工具集上进行了扩展,方式如下:
- 除了基本的矩形、椭圆、线条和涂鸦工具外,此版本还增加了折线、填充椭圆、填充矩形、文本和图像工具。
- 多重绘图图层
- 缩放
- 平移
- 旋转
在本文中,我将描述图层的实现方式,以及文本和图像工具。
背景
有关基本应用程序的构建方式、类结构等详细信息,请参阅原始 DrawTools 文章。
另外,假定读者对 GDI+ 的基础知识有实际的理解,包括矩阵。有关 GDI+ 的出色介绍,请参阅 www.bobpowell.net。
实现图层
为应用程序添加图层需要添加两个类:Layer
和 Layers
。其中 Layer
定义单个图层,而 Layers
则定义 ArrayList
中图层的集合。
每个 Layer
暴露以下属性:
private string _name;
private bool _isDirty;
private bool _visible;
private bool _active;
private GraphicsList _graphicsList;
请注意,Layer
包含 GraphicsList
——这是整个关键。每个 Layer
包含自己的绘图对象列表,而不是 DrawArea
。DrawArea
被修改为声明一个 Layers
集合,而不是一个 GraphicsList
集合。
// Define the Layers collection
private Layers _layers;
当 DrawArea
初始化时,Layers
会通过创建第一个 Layer
并将其设置为 Active
(活动)和 Visible
(可见)来初始化。
public DrawArea()
{
// create list of Layers, with one default active visible layer
_layers = new Layers();
_layers.CreateNewLayer("Default");
_panning = false;
_panX = 0;
_panY = 0;
// This call is required by the Windows.Forms Form Designer.
InitializeComponent();
}
在 Layers
类中,CreateNewLayer()
方法实际创建新的 Layer
。
/// <summary>
/// Create a new layer at the head of the layers list and set it
/// to Active and Visible.
/// </summary>
/// <param name="theName">The name to assign to the new layer</param>
public void CreateNewLayer(string theName)
{
// Deactivate the currently active Layer
if(layerList.Count > 0)
((Layer)layerList[ActiveLayerIndex]).IsActive = false;
// Create new Layer, set it visible and active
Layer l = new Layer();
l.IsVisible = true;
l.IsActive = true;
l.LayerName = theName;
// Initialize empty GraphicsList for future objects
l.Graphics = new GraphicsList();
// Add to Layers collection
this.Add(l);
}
请注意,任何一个或所有 Layers
可以同时可见,但任何时候只能有一个 Layer
是活动的。
您可以通过单击应用程序窗口底部的“当前图层”名称来控制示例应用程序中的 Layers
。单击名称(“Default
”)即可打开 Layers
对话框。

在此对话框中,您可以添加新的 Layers
,更改 Layer
的名称,并更改 Layer
的可见性和哪个 Layer
是活动的。每当您单击“添加图层”按钮时,“新图层”列将被选中。要删除图层,只需选中“删除”列,然后通过“关闭”按钮关闭对话框。请记住,任何时候只能有一个图层是活动的。如果您尝试使多个图层活动,系统会提醒您。另外请注意,活动的 Layer
必须是可见的。
应用程序运行时,每个绘制的对象都会被添加到活动 Layer
所维护的 GraphicsList
中。请注意,在保存和重新打开绘图文件时,此关系会得以保留。
当您想要在另一张图片“之上”绘制时,图层会非常方便。例如,本文顶部的图像包含两个图层。下图显示了同一张图片,但背景图层已关闭。

这是同一幅图,其中绘图图层不可见,背景图层可见。

位于可见但非活动图层上的对象无法被选中、移动、删除等。
每个绘图对象都通过 ToolObject
类中的 AddNewObject()
方法被添加到正确的 Layer
中。
protected void AddNewObject(DrawArea drawArea, DrawObject o)
{
int al = drawArea.TheLayers.ActiveLayerIndex;
drawArea.TheLayers[al].Graphics.UnselectAll();
o.Selected = true;
o.Dirty = true;
drawArea.TheLayers[al].Graphics.Add(o);
drawArea.Capture = true;
drawArea.Refresh();
}
实现缩放、平移和旋转
缩放、平移和旋转是通过向 MainForm
和 DrawArea
类添加一些变量和代码来实现的。
缩放由窗体上的按钮控制,当按下 Ctrl 键时,也可以通过鼠标滚轮进行控制。
平移由窗体上的“手形”按钮控制,可以通过右键单击取消。
旋转由窗体上的按钮控制——请注意,旋转会影响整个绘图。
以下是这三者结合使用的示例:

此代码的核心是 BackTrackMouse()
方法,它接收“视在”鼠标位置,并根据当前的缩放级别、平移位置和旋转将其转换为有效点。
/// <summary>
/// Back Track the Mouse to return accurate coordinates regardless of
/// zoom or pan effects.
/// Courtesy of BobPowell.net <seealso cref="http://www.bobpowell.net/backtrack.htm"/>
/// </summary>
/// <param name="p">Point to backtrack</param>
/// <returns>Backtracked point</returns>
public Point BackTrackMouse(Point p)
{
// Backtrack the mouse...
Point[] pts = new Point[] { p };
Matrix mx = new Matrix();
mx.Translate(-this.ClientSize.Width / 2, -this.ClientSize.Height / 2,
MatrixOrder.Append);
mx.Rotate(_rotation, MatrixOrder.Append);
mx.Translate(this.ClientSize.Width / 2 + _panX, this.ClientSize.Height / 2 +
_panY, MatrixOrder.Append);
mx.Scale(_zoom, _zoom, MatrixOrder.Append);
mx.Invert();
mx.TransformPoints(pts);
return pts[0];
}
此例程来自 Bob Powell 的优秀网站。通过使用 GDI+ Matrix 类,传递给此方法的鼠标点会根据当前的 PanX、PanY、Zoom 和 Rotation 值进行移动(Translate)、旋转和缩放。要记住的重要一点是,任何时候您需要确定鼠标指针在绘图中的实际位置时,都必须调用此方法。您会在程序中,尤其是在 DrawArea
类以及其他类中看到此方法的用法。此处展示了其用法的一个示例:
private void DrawArea_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
lastPoint = BackTrackMouse(e.Location);
if (e.Button == MouseButtons.Left)
tools[(int)activeTool].OnMouseDown(this, e);
else if (e.Button == MouseButtons.Right)
{
if (_panning == true)
_panning = false;
ActiveTool = DrawArea.DrawToolType.Pointer;
}
//OnContextMenu(e);
}
当前的缩放级别由以下简单例程控制:
private void AdjustZoom(float _amount)
{
drawArea.Zoom += _amount;
if (drawArea.Zoom < .1f)
drawArea.Zoom = .1f;
if (drawArea.Zoom > 10)
drawArea.Zoom = 10f;
drawArea.Invalidate();
SetStateOfControls();
}
然后在 DrawArea.Paint()
方法中,使用缩放、平移和旋转值来修改画布的绘制方式。
private void DrawArea_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
Matrix mx = new Matrix();
mx.Translate(-this.ClientSize.Width / 2, -this.ClientSize.Height / 2,
MatrixOrder.Append);
mx.Rotate(_rotation, MatrixOrder.Append);
mx.Translate(this.ClientSize.Width / 2 + _panX, this.ClientSize.Height / 2 +
_panY, MatrixOrder.Append);
mx.Scale(_zoom, _zoom, MatrixOrder.Append);
e.Graphics.Transform = mx;
SolidBrush brush = new SolidBrush(Color.FromArgb(255, 255, 255));
e.Graphics.FillRectangle(brush,
this.ClientRectangle);
// Draw objects on each layer, in succession so we get the correct layering.
// Only draw layers that are visible
if (_layers != null)
{
int lc = _layers.Count;
for (int i = 0; i < lc; i++)
{
if(_layers[i].IsVisible == true)
if(_layers[i].Graphics != null)
_layers[i].Graphics.Draw(e.Graphics);
}
}
DrawNetSelection(e.Graphics);
brush.Dispose();
}
更新 - 2007/8/25 - 单个对象旋转和错误修复
此次更新的主要进展是能够旋转单个对象——当选中一个或多个对象时,点击旋转工具将旋转这些对象,而不是整个绘图表面。
然而,有一个注意事项——旋转对象的选择框不会被旋转。如果有人能在这方面提供帮助,我将不胜感激!
此次更新还包括用户报告的一些小型错误修复——感谢所有报告的用户!
历史
- 3/6/2007
- 原始文章已上传至 The Code Project。
- 3/6/2007
- 更新以包含更多关于缩放/平移/旋转的信息。
- 8/25/2007
- 更新了单个对象旋转功能。
- 9/27/2007
- 添加了指向新源代码的缺失链接。
- 12/23/2009
- 添加了鼠标悬停在对象上时出现的工具提示控件。工具提示显示
Rectangle
、Ellipse
和Image
对象的中心坐标。对于其他对象,工具提示显示起始和结束坐标。Text
对象不显示工具提示。
这是通过将工具提示控件添加到ToolPointer
类来实现的。每个Draw
Object 填充TipText
属性,并且当工具提示显示和从画布移除时,ToolPointer
类中的MouseMove
事件会触发。此实现并不完美,因为工具提示显示时会闪烁,因此仅作为一种显示对象信息方式的示例。
也许更好的方法是显示对象信息在一个单独的“信息窗口”中,并且仅在对象被选中时显示。 - 详情请参阅新源代码。
- 6/23/2010
- 更新了项目以包含对象排序修复,该修复更正了打开文件时对象的堆叠方式。
- 将项目更新至 Visual Studio 2010。
- 详情请参阅新源代码。
- 10/4/2011
- 修正了与图层和分层相关的几个问题。