计算直线的精确像素

本文关键字:像素 计算 | 更新日期: 2023-09-27 18:20:15

假设我想尝试画一条直线,尽管有任何角度的

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实现命中测试。

计算直线的精确像素

有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++和Javascript版本,这两个版本都非常接近C#。我想在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个像素。

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

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

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

在屏幕外,您可以关闭抗锯齿,这样您就可以在编写颜色值时准确地读取这些值。现在您可以简单地从位图中读取。如果需要测试多行,请将索引值放入颜色中。

有更复杂的方法可以做到这一点,但简单的方法只是处理自定义控件的点击事件。换句话说,为Control基类引发的MouseClick事件添加一个处理程序。通过这种方式,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属性设置为实际的逻辑维度(即线的宽度和厚度)。

如果你只想看看鼠标是否在线段附近,你不需要确切地知道像素在哪里——你只需要知道它们是否在一定的距离内。

这是一个我敲在一起的小班级。它只是使用直线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));
}