2

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

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.

4

5 回答 5

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);
于 2013-06-28T18:53:31.390 回答
0

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

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

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

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

于 2013-06-28T19:15:24.687 回答
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));
}
于 2013-06-28T20:07:25.063 回答
0

你应该看看这个问题,它提供了一些代码来计算从一个点到给定线段的距离,并带有起点和终点。它提供了非常接近 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 个像素。

于 2013-06-28T19:08:08.887 回答
0

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

于 2013-06-28T19:47:08.397 回答