65.9K
CodeProject 正在变化。 阅读更多。
Home

在面板上绘图

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (20投票s)

2011年5月23日

CPOL

6分钟阅读

viewsIcon

90281

downloadIcon

8847

如何创建一个可以绘制的面板。

Paint_on_a_panel.jpg

引言

在本文中,我们将介绍如何设计和实现使用鼠标在面板上绘画的功能,如何记录这些绘图数据,绘制线条以及创建橡皮擦工具来移除这些线条。更重要的是,我们还将存储附加信息,如线条的大小和颜色,最后通过告知用户当前的绘图颜色和大小来完成整个功能。

这种图形绘制方法更倾向于矢量图形,其中点是浮点值,通过插值而不是直接的光栅化位图值来绘制。

背景

本文内容非常基础,因此不需要任何先前的绘图知识,但了解一些绘图知识将有助于扩展本文的内容。

Using the Code

这里的代码经过精心设计,可以轻松地移植到各种其他项目中,只需很少的麻烦,并且可以扩展以包含其他非标准绘图功能,如文本。

为了实现这一点,我们有两个主要的类,其中一个类负责存储、添加、删除和获取单个形状数据,而另一个类则更多地用作结构,存储颜色、线宽、位置以及它是哪个形状段等数据。

Classes.JPG

类 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日:初次发布
© . All rights reserved.