计算线的精确像素

时间:2013-06-28 18:12:06

标签: c# .net winforms gdi+

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

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

5 个答案:

答案 0 :(得分: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);

答案 1 :(得分:0)

您应该查看this question,它提供了一些代码来计算从一个点到具有起点和终点的给定线段的距离。它提供的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像素。

答案 2 :(得分:0)

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

  1. 一次屏幕,
  2. 一次到屏幕外缓冲区。
  3. 显而易见的方法是使缓冲区与控件的客户端矩形相同。

    在屏幕外,您可以关闭抗锯齿功能,以便您可以完全按照编写颜色值的方式读取颜色值。现在您只需从位图中读取即可。如果需要测试多行,请将索引值放在颜色中。

答案 3 :(得分:0)

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

答案 4 :(得分:0)

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

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