7
votes

I have an arrow drawn between two objects on a Winform.

What would be the simplest way to determine that my mouse is currently hovering over, or near, this line.

I have considered testing whether the mouse point intersects a square defined and extrapolated by the two points, however this would only be feasible if the two points had very similar x or y values.

I am thinking, also, this problem is probably more in the realms of linear algebra rather than simple trigonometry, and whilst I do remember the simpler aspects of matrices, this problem is beyond my knowledge of linear algebra.

On the other hand, if a .NET library can cope with the function, even better.

EDIT Thanks for the answers, there were a few very good ones all deserving being tagged as answered.

I chose Coincoin's answer as accepted, as I like that it could be applied to any shape drawn, however ended up implementing Tim Robinson's equation, as it appeared much more efficient to with a simple equation rather than newing up graphics paths and pens, as in my case I need to do it onMouseMove for 1-n different relationships (obviously there would be some caching and optimisations, but the point still remains)

The main issue with the equation was that it appeared to treat the line as infinite, so I added a bounds test as well.

The code (initial cut, I'll probably neaten it a bit), for those interested, is below

    if (Math.Sqrt( Math.Pow(_end.X - _start.X, 2) + 
           Math.Pow(_end.Y - _start.Y, 2) ) == 0)
    {
        _isHovering =
            new RectangleF(e.X, e.Y, 1, 1).IntersectsWith(_bounds);
    }
    else
    {
        float threshold = 10.0f;

        float distance = (float)Math.Abs( 
            ( ( (_end.X - _start.X) * (_start.Y - e.Y) ) -
            ( (_start.X - e.X) * (_end.Y - _start.Y) ) ) /
            Math.Sqrt( Math.Pow(_end.X - _start.X, 2) + 
            Math.Pow(_end.Y - _start.Y, 2) ));

        _isHovering = (
            distance <= threshold &&
                new RectangleF(e.X, e.Y, 1, 1).IntersectsWith(_bounds)
            );
    }

and _bounds is defined as:

    _bounds = new Rectangle(
    Math.Min(_start.X, _end.X),
    Math.Min(_start.Y, _end.Y),
    Math.Abs(_start.X - _end.X), Math.Abs(_start.Y - _end.Y));
5
I need, also, to take into account the threshold when I do the div by 0 special case checkjohnc

5 Answers

7
votes

If you want to easly make hit tests on arbitrary drawn shapes, you can create a path containing your drawing, then widden the path and make a visibility test using only framework functions.

For instance, here we create a path with a line:

GraphicsPath path = new GraphicsPath();

path.AddLine(x1, y1, x2, y2);
path.CloseFigure();

Then, widen the path and create a region for the hit test:

path.Widen(new Pen(Color.Black, 3));
region = new Region(path);

Finally, the hit test:

region.IsVisible(point);

The advantage of that method is it can easily extend to splines, arrows, arc, pies or pretty much anything drawable with GDI+. The same path can be used in both the HitTest and Draw logic by extracting it.

Here is the code combining it all:

public GraphicsPath Path
{
    get { 
        GraphicsPath path = new GraphicsPath();
        path.AddLine(x1, y1, x2, y2);
        path.CloseFigure();

        return path;
    }
}

bool HitTest(Point point)
{
    using(Pen new pen = Pen(Color.Black, 3))
    using(GraphicsPaht path = Path)
    {
        path.Widen(pen);

        using(Region region = new Region(path))
            return region.IsVisible(point);
    }
}


void Draw(Graphics graphics)
{
    using(Pen pen = new Pen(Color.Blue, 0))
    using(GraphicsPaht path = Path)
        graphics.DrawPath(pen, path);
}
4
votes

To answer "Is the mouse hovering over this line?", you need to check for point-line intersection. However, since you're asking "is the mouse near the line?", it sounds like you want to calculate the distance between the mouse point and the line.

Here's a reasonably thorough explanation of point-line distance: http://mathworld.wolfram.com/Point-LineDistance2-Dimensional.html

I'd say you need to implement this formula in your code: (stolen from wolfram.com)

Where:

  • (x0, x0) is the location of the mouse pointer
  • (x1, y1) is one end of the line
  • (x2, y2) is the other end of the line
  • |n| is Math.Abs(n)
  • The bottom half is Math.Sqrt
  • You can ignore the |v.r| if you want
2
votes

I would calculate the Slope-Intercept equation (y = mx + b) for my line and then use that to test the mouse coordinates. You could easily put a range around y to see if you're "close."

Edit for sample.

I think something like this works:

PointF currentPoint;
PointF p1, p2;
float threshold = 2.0f;
float m = (p1.Y - p2.Y) / (p1.X - p2.X);
float b = p1.Y - (m * p1.X);

if (Math.Abs(((m * currentPoint.X) + b) - currentPoint.Y) <= threshold)
{
    //On it.
}
1
votes

You need to construct two (notional) boundary lines parallel to the ideal path. Then you only need calculate, for each mouse position, whether the mouse is outside or inside the channel formed by those lines.

You don't need to calculate the distance from the mouse to the main line.

0
votes

Check out MouseEnter(object sender, EventArgs e). Trap when it "enters" the control area.