【问题标题】:Calculating Exact Pixels for a line计算直线的精确像素
【发布时间】:2013-06-28 18:12:06
【问题描述】:

说我想尝试做一条直线,尽管是任何角度

public class Line : Control
{
    public Point start { get; set; }
    public Point end { get; set; }
    public Pen pen = new Pen(Color.Red);

    protected override void OnPaint(PaintEventArgs e)
    {
        e.Graphics.DrawLine(pen, start, end);
        base.OnPaint(e);
    }
}

此行是在自定义控件上创建的。

现在我怎样才能计算出线条所在的确切像素,以便我可以使用MouseMove 进行命中测试。

【问题讨论】:

  • 所有这些听起来很像你是reinventing the wheel。忘记 winforms 使用 WPF,它已经完成了这一切。
  • 您是否想查看鼠标是否在这条随机线的 X 像素范围内?
  • @Tombala 是的,我可以处理所有其他事情,我只需要知道如何计算两点之间的像素
  • @WinCoder - 这取决于你所关心的“线”的抽象。例如,您可能希望将线视为“数学”线,宽度为 0,并想知道所选像素是否与该理论线相交。或者,您可能会关心由e.Graphics.DrawLine(pen, start, end) 命令更改颜色的实际像素,这取决于抗锯齿等设置。
  • 你应该为此使用数学,计算你的点的法线。法线长度是否小于 1,或者无论您想要什么精度,我们都称其为碰撞。或者超级简单,计算两个距离(从鼠标位置到线的每个点)。如果这些距离的总和接近线长,那么您也会发生碰撞

标签: c# .net winforms gdi+


【解决方案1】:

有 Win32 调用用于枚举将使用 GDI 调用绘制的线条的像素。我相信这是您想要完成的最佳技术。请参阅LineDDA 及其关联的回调LineDDAProc

以下是您从 C# 中使用它的方法。请注意,根据 LineDDA 的文档,输出中不包含终点。

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.InteropServices;

public static List<Point> GetPointsOnLine(Point point1, Point point2)
{
    var points = new List<Point>();
    var handle = GCHandle.Alloc(points);
    try
    {
        LineDDA(point1.X, point1.Y, point2.X, point2.Y, GetPointsOnLineCallback, GCHandle.ToIntPtr(handle));
    }
    finally
    {
        handle.Free();
    }
    return points;
}

private static void GetPointsOnLineCallback(int x, int y, IntPtr lpData)
{
    var handle = GCHandle.FromIntPtr(lpData);
    var points = (List<Point>) handle.Target;
    points.Add(new Point(x, y));
}

[DllImport("gdi32.dll")]
private static extern bool LineDDA(int nXStart, int nYStart, int nXEnd, int nYEnd, LineDDAProc lpLineFunc, IntPtr lpData);

// The signature for the callback method
private delegate void LineDDAProc(int x, int y, IntPtr lpData);

【讨论】:

  • 好吧,谢谢提醒,但我对 C# 还是很陌生,更不用说 Win32,因此我对这些东西一无所知。无论如何,谢谢给了我另一种方法,如果其他一切都干涸了,我可以使用它。
  • 再次感谢您的帮助。
  • 考虑接受这个作为答案,或者调整问题的标题以更好地表明这是一个命中测试问题(然后接受其他人的答案)。这将帮助其他人稍后找到答案。
【解决方案2】:

您应该查看this question,它提供了一些代码来计算从一个点到具有起点和终点的给定线段的距离。它提供了非常接近 C# 的 C++ 和 Javascript 版本。我会在您的 Line 类中添加一个使用该代码的方法:

public class Line : Control
{
    public Point start { get; set; }
    public Point end { get; set; }
    public Pen pen = new Pen(Color.Red);

    protected override void OnPaint(PaintEventArgs e)
    {
        e.Graphics.DrawLine(pen, start, end);
        base.OnPaint(e);
    }

    public float DistanceToLine(Point x)
    {
        // do your distance calculation here based on the link provided.
    }
}

然后检查距离是否小于 2 像素。

【讨论】:

    【解决方案3】:

    如果你真的想这样做,绘制你的控件两次:

    1. 一屏,
    2. 一次到屏幕外缓冲区。

    显而易见的方法是使缓冲区的大小与控件的客户矩形相同。

    在屏幕外,您可以关闭抗锯齿功能,这样您就可以完全按照您所写的方式读取颜色值。现在您可以简单地从位图中读取。如果您需要点击测试多行,请将索引值放在颜色中。

    【讨论】:

    • 嗯,我认为做事的数学方法似乎很容易。谢谢。
    【解决方案4】:

    有更复杂的方法可以做到这一点,但简单的方法就是为您的自定义控件处理点击事件。换句话说,为MouseClick event 添加由Control 基类引发的处理程序。这样,Windows 就会为您完成所有命中测试。

    如果用户单击控件上的任意位置,将引发MouseClick 事件,您可以随意处理它。否则,不会引发任何事件。简单的缩影。

    MouseClick 事件处理程序中,您将在客户端坐标中获得一个点 (e.Location),这意味着该位置相对于客户端控件的左上角。

    出于测试目的,我只是在一个空表单中添加了一个Label 控件,关闭了AutoSize,并将BackColor 设置为红色。然后我让它看起来像一条线,并为MouseClick 事件添加了一个处理程序。处理程序如下所示:

    private void redLabel_MouseClick(object sender, MouseEventArgs e)
    {
       // Fired whenever the control is clicked; e.Location gives the location of
       // the mouse click in client coordinates.
       Debug.WriteLine("The control was clicked at " + e.Location);
    }
    

    这种简单的命中测试方法依赖于这样一个事实:就 Windows 而言,控件的物理边界与其逻辑边界相同。因此,要使其与您的自定义控件一起使用,您需要确保将其 Size 属性设置为其实际的逻辑尺寸(即线条的宽度和粗细)。

    【讨论】:

    • 这是真正的问题,到目前为止,我一直无法说为对角线制作对角线组件。更复杂的是,用户可以制作任意线条,这意味着控件的形状不仅必须在运行时生成,而且还必须任意。
    【解决方案5】:

    如果您只想查看鼠标是否靠近线段,则无需知道像素的确切位置 - 您只需知道它们在逻辑上是否在一定距离内。

    这是我拼凑的一个小班。它只是使用直线y = mx+c 的普通公式来计算任何特定点是否在直线的一定距离(公差)内。

    给定两个点,p1p2,它们是您要进行命中测试的直线端点的坐标,您可以这样初始化它:

    var hitTest = new LineIntersectionChecker(p1, p2);
    

    然后检查另一个点,p 是否在线:

    if (hitTest.IsOnLine(p))
        ...
    

    类实现:

    public sealed class LineIntersectionChecker
    {
        private readonly PointF _p1;
        private readonly PointF _p2;
        private readonly double _slope;
        private readonly double _yIntersect;
        private readonly double _tolerance;
        private readonly double _x1;
        private readonly double _x2;
        private readonly double _y1;
        private readonly double _y2;
        private readonly bool   _isHorizontal;
        private readonly bool   _isVertical;
    
        public LineIntersectionChecker(PointF p1, PointF p2, double tolerance = 1.0)
        {
            _p1 = p1;
            _p2 = p2;
            _tolerance = tolerance;
    
            _isVertical   = (Math.Abs(p1.X - p2.X) < 0.01);
            _isHorizontal = (Math.Abs(p1.Y - p2.Y) < 0.01);
    
            if (_isVertical)
            {
                _slope      = double.NaN;
                _yIntersect = double.NaN;
            }
            else // Useable.
            {
                _slope = (p1.Y - p2.Y)/(double) (p1.X - p2.X);
                _yIntersect = p1.Y - _slope * p1.X ;
            }
    
            if (_p1.X < _p2.X)
            {
                _x1 = _p1.X - _tolerance;
                _x2 = _p2.X + _tolerance;
            }
            else
            {
                _x1 = _p2.X - _tolerance;
                _x2 = _p1.X + _tolerance;
            }
    
            if (_p1.Y < _p2.Y)
            {
                _y1 = _p1.Y - _tolerance;
                _y2 = _p2.Y + _tolerance;
            }
            else
            {
                _y1 = _p2.Y - _tolerance;
                _y2 = _p1.Y + _tolerance;
            }
        }
    
        public bool IsOnLine(PointF p)
        {
            if (!inRangeX(p.X) || !inRangeY(p.Y))
                return false;
    
            if (_isHorizontal)
                return inRangeY(p.Y);
    
            if (_isVertical)
                return inRangeX(p.X);
    
            double expectedY = p.X*_slope + _yIntersect;
    
            return (Math.Abs(expectedY - p.Y) <= _tolerance);
        }
    
        private bool inRangeX(double x)
        {
            return (_x1 <= x) && (x <= _x2);
        }
    
        private bool inRangeY(double y)
        {
            return (_y1 <= y) && (y <= _y2);
        }
    }
    

    您可以通过使用要进行命中测试的线两端的点来实例化它来使用它,然后为要检查线的每个点调用IsOnLine(p)

    您会从 MouseMove 或 MouseDown 消息中获得要检查的点数。

    请注意,您可以在构造函数中设置不同的容差。我将其默认为 1,因为“在 1 像素内”似乎是一个合理的默认值。

    这是我测试它的代码:

    double m = 0.5;
    double c = 1.5;
    
    Func<double, float> f = x => (float)(m*x + c);
    
    Random rng = new Random();
    
    PointF p1 = new PointF(-1000, f(-1000));
    PointF p2 = new PointF(1000, f(1000));
    
    var intersector = new LineIntersectionChecker(p1, p2, 0.1);
    
    Debug.Assert(intersector.IsOnLine(new PointF(0f, 1.5f)));
    
    for (int i = 0; i < 1000; ++i)
    {
        float x = rng.Next((int)p1.X+2, (int)p2.X-2);
        PointF p = new PointF(x, f(x));
    
        Debug.Assert(intersector.IsOnLine(p));
    }
    

    【讨论】:

      猜你喜欢
      • 2014-09-01
      • 1970-01-01
      • 2013-10-02
      • 2016-12-01
      • 2018-09-17
      • 2021-09-20
      • 1970-01-01
      • 2021-10-14
      • 2012-09-30
      相关资源
      最近更新 更多