在面板上绘图






4.78/5 (20投票s)
如何创建一个可以绘制的面板。

引言
在本文中,我们将介绍如何设计和实现使用鼠标在面板上绘画的功能,如何记录这些绘图数据,绘制线条以及创建橡皮擦工具来移除这些线条。更重要的是,我们还将存储附加信息,如线条的大小和颜色,最后通过告知用户当前的绘图颜色和大小来完成整个功能。
这种图形绘制方法更倾向于矢量图形,其中点是浮点值,通过插值而不是直接的光栅化位图值来绘制。
背景
本文内容非常基础,因此不需要任何先前的绘图知识,但了解一些绘图知识将有助于扩展本文的内容。
Using the Code
这里的代码经过精心设计,可以轻松地移植到各种其他项目中,只需很少的麻烦,并且可以扩展以包含其他非标准绘图功能,如文本。
类
为了实现这一点,我们有两个主要的类,其中一个类负责存储、添加、删除和获取单个形状数据,而另一个类则更多地用作结构,存储颜色、线宽、位置以及它是哪个形状段等数据。

类 Shapes
方法 | 功能 |
GetShape() |
返回索引形状。 |
NewShape() |
使用函数参数(位置、宽度、颜色、形状段索引)创建一个新形状。 |
NumberOfShapes() |
返回当前存储的形状数量。 |
RemoveShape() |
移除距离某个点在特定阈值内的任何点,并重新排序剩余点的形状段索引,这样我们就不会出现连接奇怪线条的问题。 |
public class Shapes
{
private List _Shapes; //Stores all the shapes
public Shapes()
{
_Shapes = new List();
}
//Returns the number of shapes being stored.
public int NumberOfShapes()
{
return _Shapes.Count;
}
//Add a shape to the database, recording its position,
//width, colour and shape relation information
public void NewShape(Point L, float W, Color C, int S)
{
_Shapes.Add(new Shape(L,W,C,S));
}
//returns a shape of the requested data.
public Shape GetShape(int Index)
{
return _Shapes[Index];
}
//Removes any point data within a certain threshold of a point.
public void RemoveShape(Point L, float threshold)
{
for (int i = 0; i < _Shapes.Count; i++)
{
//Finds if a point is within a certain distance of the point to remove.
if ((Math.Abs(L.X - _Shapes[i].Location.X) < threshold) &&
(Math.Abs(L.Y - _Shapes[i].Location.Y)< threshold))
{
//removes all data for that number
_Shapes.RemoveAt(i);
//goes through the rest of the data and adds an extra
//1 to defined them as a separate shape and shuffles on the effect.
for (int n = i; n < _Shapes.Count; n++)
{
_Shapes[n].ShapeNumber += 1;
}
//Go back a step so we don't miss a point.
i -= 1;
}
}
}
}
类 Shape
变量 | 类型 | 目的 |
Colour |
Color | 保存此线条部分的颜色。 |
Location |
Point | 线条的位置。 |
ShapeNumber |
Int | 此线条部分所属的形状编号。 |
宽度 |
Float | 在该点绘制线条的宽度。 |
public class Shape
{
public Point Location; //position of the point
public float Width; //width of the line
public Color Colour; //colour of the line
public int ShapeNumber; //part of which shape it belongs to
//CONSTRUCTOR
public Shape(Point L, float W, Color C, int S)
{
Location = L; //Stores the Location
Width = W; //Stores the width
Colour = C; //Stores the colour
ShapeNumber = S; //Stores the shape number
}
}
设置
在 C# 或 .NET 中进行绘制时,建议强制绘图表面使用双缓冲来减少重绘时的闪烁量,但这会带来内存开销。我们还应该定义更多变量来处理鼠标位置、当前绘图形状、当前颜色等条件。
private Shapes DrawingShapes = new Shapes(); //Stores all the drawing data
private bool IsPainting = false; //Is the mouse currently down
private Point LastPos = new Point(0, 0); //Last Position, used to cut down
//on repetitive data.
private Color CurrentColour = Color.Black; //Default Colour
private float CurrentWidth = 10; //Default Pen width
private int ShapeNum = 0; //record the shapes so they can be
//drawn separately.
public Form1()
{
InitializeComponent();
//Set Double Buffering
panel1.GetType().GetMethod("SetStyle",
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.NonPublic).Invoke(panel1,
new object[]{ System.Windows.Forms.ControlStyles.UserPaint |
System.Windows.Forms.ControlStyles.AllPaintingInWmPaint |
System.Windows.Forms.ControlStyles.DoubleBuffer, true });
}
DrawingShapes
是 Shapes 类的一个实例,它将允许我们存储所有必要的绘图信息,并允许重绘函数稍后使用此变量来绘制线条。
事件
有了存储绘图点数据的能力,我们需要增强绘图面板的事件处理器,以适应 MouseDown
(按下鼠标按钮时)、MouseMove
(鼠标在绘图面板上移动时)和 MouseUp
(松开鼠标按钮时)的函数。
当鼠标在绘图面板上按下时,我们希望开始记录数据,当鼠标在按下状态下移动时,我们希望记录鼠标的位置,当鼠标按钮被抬起时,这标志着绘图线条的结束。
MouseDown
private void panel1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
//set it to mouse down, illustrate the shape being drawn and reset the last position
IsPainting = true;
ShapeNum++;
LastPos = new Point(0, 0);
}
MouseMove
protected void panel1_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
{
//PAINTING
if (IsPainting)
{
//check it's not at the same place it was last time,
//saves on recording more data.
if (LastPos != e.Location)
{
//set this position as the last position
LastPos = e.Location;
//store the position, width, colour and shape relation data
DrawingShapes.NewShape(LastPos, CurrentWidth,
CurrentColour, ShapeNum);
}
}
//refresh the panel so it will be forced to re-draw.
panel1.Refresh();
}
MouseUp
private void panel1_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{
if (IsPainting)
{
//Finished Painting.
IsPainting = false;
}
}
现在我们可以存储点数据了,最后一步是绘制线条。为此,需要在绘图面板上添加另一个事件处理器,用于 Paint
(每次面板重绘时调用)。在这里,我们使用收集到的数据,根据保存的线宽和线条颜色连接这些点。形状编号信息在这里特别有用,因为我们不希望在两个不相关的点之间绘制线条。
Paint
//DRAWING FUNCTION
private void panel1_Paint(object sender, PaintEventArgs e)
{
//Apply a smoothing mode to smooth out the line.
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
//DRAW THE LINES
for (int i = 0; i < DrawingShapes.NumberOfShapes()-1; i++)
{
Shape T = DrawingShapes.GetShape(i);
Shape T1 = DrawingShapes.GetShape(i+1);
//make sure shape the two adjoining shape numbers are part of the same shape
if (T.ShapeNumber == T1.ShapeNumber)
{
//create a new pen with its width and colour
Pen p = new Pen(T.Colour, T.Width);
p.StartCap = System.Drawing.Drawing2D.LineCap.Round;
p.EndCap = System.Drawing.Drawing2D.LineCap.Round;
//draw a line between the two adjoining points
e.Graphics.DrawLine(p, T.Location, T1.Location);
//get rid of the pen when finished
p.Dispose();
}
}
}
添加橡皮擦工具
现在我们可以在面板上绘制了,是时候以类似的方式来擦除线条了。为此,我们需要稍微修改我们的代码以适应这种新的交互。我实现的方式是添加 2 个额外的变量,它们都在程序开始时定义。
Brush
是一个 **boolean** 值,表示当前使用的绘图工具或橡皮擦工具。
IsErasing
是一个 **boolean** 值,其使用方式与 IsPainting
变量完全相同。
Brush
变量的使用改变了一些事情,最值得注意的是 MouseDown
事件处理器。当鼠标按下时,我们不再将 IsPainting
变量设置为 true
,而是需要检查它是绘图还是擦除。函数在修改后应如下所示:
private void panel1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
//If we're painting...
if (Brush)
{
//set it to mouse down, illustrate the shape being drawn
//and reset the last position
IsPainting = true;
ShapeNum++;
LastPos = new Point(0, 0);
}
//but if we're erasing...
else
{
IsEraseing = true;
}
}
MouseMove
函数有一个如下所示的修改:
if (IsEraseing)
{
//Remove any point within a certain distance of the mouse
DrawingShapes.RemoveShape(e.Location,10);
}
以及 MouseUp
if (IsEraseing)
{
//Finished Erasing.
IsEraseing = false;
}
绘制鼠标光标
在面板上移除和绘制新线条很方便,但用户很难直观地看到他们绘画或擦除线条的大小。为此,我们需要对程序进行最后一些微调。这种方法对于大型应用程序来说有点粗暴,但对于小型应用程序来说足够了。为了实现这个功能,还需要另外两个变量。
MouseLoc
是一个 **point** 变量,这意味着它保存两个整数值,非常适合存储位置坐标,并将存储当前鼠标坐标。
IsMouseing
是一个 **boolean** 值,用于决定是否绘制鼠标“绘画指针”。
为了更好地隐藏和显示鼠标光标,当它进入或离开绘图面板时,以及告诉重绘器绘制“绘画指针”,使用了两个事件处理器。
MouseEnter(隐藏鼠标光标)
private void panel1_MouseEnter(object sender, EventArgs e)
{
//Hide the mouse cursor and tell the re-drawing function to draw the mouse
Cursor.Hide();
IsMouseing = true;
}
MouseLeave(显示鼠标光标)
private void panel1_MouseLeave(object sender, EventArgs e)
{
//show the mouse, tell the re-drawing function
//to stop drawing it and force the panel to re-draw.
Cursor.Show();
IsMouseing = false;
panel1.Refresh();
}
需要在 MouseMove
函数中进行两个小的更改来更新 MouseLoc
,然后最终绘制它,应添加以下行:
MouseLoc = e.Location;
在 Paint
事件的底部,应添加以下行,这将绘制鼠标指针尖端的中心圆,圆的大小与绘图宽度相同。
if (IsMouseing)
{
e.Graphics.DrawEllipse(new Pen(Color.White, 0.5f),
MouseLoc.X - (CurrentWidth / 2),
MouseLoc.Y - (CurrentWidth / 2), CurrentWidth, CurrentWidth);
}
重要的是要注意,如果鼠标光标的绘制发生在线条绘制之前,线条将绘制在光标之上,因此将不可见。
关注点
这个项目相当有趣,没有什么特别具有挑战性的,但尝试优化它以实现我最初的想法很有意思。通过创建一个小型矢量艺术包或引入图层,可以进一步扩展这段代码,看看还能实现什么。对我而言,我一直在使用它来让用户为节点图和图像添加注释,并与之一起存储矢量数据。
历史
- 2011年5月23日:初次发布