c# - 删除已经绘制的形状

标签 c# system.drawing

<分区>

我正在使用 System.Drawing 命名空间在 C# 中编写自己的 Paint 代码,到目前为止,除了橡皮擦工具之外,基本上一切都很顺利。

现在我的橡皮擦只是通过画一条与背景颜色相同的线来工作,所以它看起来像是在删除某些东西。

但是,我想制作一个新的橡皮擦工具,也许会有所改进。如果点击在它的范围内,这个新的橡皮擦将能够通过单击删除一个元素。 我知道,如果已经绘制了一件东西,它就在那里,什么也做不了,但我正在考虑创建一个字符串数组,我将向该数组添加新元素。例如,当我添加一条线和一个矩形时,数组的前两个元素将是:

线起点终点

矩形高宽x y

类似的东西。而当使用删除工具时,只需比较坐标即可。

有更简单的方法吗?

非常感谢!

最佳答案

是的,有。您打算做的基本上是 retained mode rendering在某种方式。您保留 Canvas 上对象的列表(或其他数据结构),您可以以任何方式重新排序或更改该列表,例如通过删除或添加对象。之后您只需重新创建绘图,即清除您的绘图区域,然后按顺序绘制列表中的每个对象。这是必要的,因为一旦您绘制了一些东西,您只有像素,并且如果您的线和矩形相交,您可能无法将线像素与矩形像素分开。

对于 GDI+,这是唯一的方法,因为您得到的不仅仅是原始绘图表面。但是,存在其他已经为您提供渲染模型的东西,例如WPF。

但是,string[] 是解决此问题的可怕 方法。通常你会有某种接口(interface),例如

public interface IShape {
  public void Draw(Graphics g);
  public bool IsHit(PointF p);
}

你的形状实现的。一条线会将其描边颜色和开始/结束坐标保持为状态,然后使用该状态在 Draw 方法中绘制自己。此外,当您想用橡皮擦点击一个形状时,您可以使用 IsHit 方法来确定是否点击了一个形状。这样每个形状都负责自己的 HitTest 。例如,一条线可以实现一点模糊,这样您就可以点击该线旁边的一点,而不必准确地点击一个像素。

无论如何,这是一般的想法。您可以根据需要将其扩展到其他想法。请注意,通过使用这种方法,您的核心代码不必知道任何可能的形状(如果您必须维护不断增长的不同的 switch 语句,比较坐标可能会有点麻烦形状)。当然,要绘制这些形状,您仍然需要更多代码,因为线条可能需要与矩形、椭圆或文本对象不同的交互方式。

我创建了一个概述上述方法的小示例 here .有趣的部分如下:

interface IShape
{
    Pen Pen { get; set; }
    Brush Fill { get; set; }
    void Draw(Graphics g);
    bool IsHit(PointF p);
}

class Line : IShape
{
    public Brush Fill { get; set; }
    public Pen Pen { get; set; }
    public PointF Start { get; set; }
    public PointF End { get; set; }

    public void Draw(Graphics g)
    {
        g.DrawLine(Pen, Start, End);
    }

    public bool IsHit(PointF p)
    {
        // Find distance to the end points
        var d1 = Math.Sqrt((Start.X - p.X) * (Start.X - p.X) + (Start.Y - p.Y) * (Start.Y - p.Y));
        var d2 = Math.Sqrt((End.X - p.X) * (End.X - p.X) + (End.Y - p.Y) * (End.Y - p.Y));

        // https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
        var dx = End.X - Start.X;
        var dy = End.Y - Start.Y;
        var length = Math.Sqrt(dx * dx + dy * dy);
        var distance = Math.Abs(dy * p.X - dx * p.Y + End.X * Start.Y - End.Y * Start.X) / Math.Sqrt(dy * dy + dx * dx);

        // Make sure the click was really near the line because the distance above also works beyond the end points
        return distance < 3 && (d1 < length + 3 && d2 < length + 3);
    }
}

public partial class Form1 : Form
{
    private ObservableCollection<IShape> shapes = new ObservableCollection<IShape>();
    private static Random random = new Random();

    public Form1()
    {
        InitializeComponent();
        pictureBox1.Image = new Bitmap(pictureBox1.Width, pictureBox1.Height);
        shapes.CollectionChanged += Shapes_CollectionChanged;
    }

    private void Shapes_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        Redraw();
    }

    public void Redraw()
    {
        using (var g = Graphics.FromImage(pictureBox1.Image))
        {
            foreach (var shape in shapes)
            {
                shape.Draw(g);
            }
        }
        pictureBox1.Invalidate();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        shapes.Add(new Line
        {
            Pen = Pens.Red,
            Start = new PointF(random.Next(pictureBox1.Width), random.Next(pictureBox1.Height)),
            End = new PointF(random.Next(pictureBox1.Width), random.Next(pictureBox1.Height))
        });
    }

    private void pictureBox1_SizeChanged(object sender, EventArgs e)
    {
        pictureBox1.Image = new Bitmap(pictureBox1.Width, pictureBox1.Height);
        Redraw();
    }

    private IShape FindShape(PointF p)
    {
        // Reverse search order because we draw from bottom to top, but we need to hit-test from top to bottom.
        foreach (var shape in shapes.Reverse())
        {
            if (shape.IsHit(p))
                return shape;
        }
        return null;
    }

    private void button1_MouseClick(object sender, MouseEventArgs e)
    {
        var shape = FindShape(e.Location);
        if (shape != null)
        {
            shape.Pen = Pens.Blue;
            Redraw();
        }
    }
}

点击按钮创建一条随机线并重新绘制。重绘就这么简单

public void Redraw()
{
    using (var g = Graphics.FromImage(pictureBox1.Image))
    {
        foreach (var shape in shapes)
        {
            shape.Draw(g);
        }
    }
    pictureBox1.Invalidate();
}

单击图片将尝试在单击点找到一个形状,如果找到一个,它会将其着色为蓝色(同时重绘)。查找项目的工作方式如下:

private IShape FindShape(PointF p)
{
    // Reverse search order because we draw from bottom to top, but we need to hit-test from top to bottom.
    foreach (var shape in shapes.Reverse())
    {
        if (shape.IsHit(p))
            return shape;
    }
    return null;
}

如您所见,实际绘制 事物并可能再次选择它们的基本部分就这样相当容易。当然,不同的绘图工具是另一回事,虽然that has solutions, too .

关于c# - 删除已经绘制的形状,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33484207/

相关文章:

c# - 使用 Visual Studio 2010 Express 创建我可以绘制的表面

c# - 为什么 Physics2D 会针对 AddForce() 与速度产生不同的结果?

c# - 从 MySql 数据创建刻度盘和图表

c# - Itextsharp - 检查添加元素是否会创建新页面

c# - C# 中的图像调整大小 - 确定调整大小尺寸(高度和宽度)的算法

c# - 使 +y 向上,移动原点 C# System.Drawing.Graphics

c# - 如何在 C# 中传递通用数组?

c# - 在 Entity Framework Core 3 中使用 TransactionScope 时 SET IDENTITY_INSERT 不起作用

c# - 在服务器桌面 session 上捕获屏幕

c# - 如果窗体上没有其他控件,图表就会消失