views:

59

answers:

5

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));
A: 

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

Russell Steen
Check MouseEnter on what? This is a line drawn by the GDI graphics object?
johnc
Ah. I didn't know you were drawing it directly.If it's a winforms control, it comes wired up with mouse events. You could possibly inherit from a control base and override drawing to pick these up.
Russell Steen
Thanks, but it's way too inefficient to use a control base in this circumstance, I'm afraid.
johnc
+4  A: 

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
Tim Robinson
I'll give it a shot and let you know. Thanks.
johnc
@Tim Robinson, I've implemented the equation, see the question
johnc
+1  A: 

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.

egrunin
Nice idea [arbitrary text to make up required comment length]
johnc
+2  A: 

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.
}
Jacob G
I like the efficiency of this one
johnc
+1 This is what I was suggesting, but he took the time to write out the math.
egrunin
This won't work for vertical lines where p1.X == p2.X, since you'll be dividing by zero. Vertical lines don't have a defined slope.
David Norman
@David: Good point. Should be really easy to test for though.
Jacob G
+7  A: 

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))
        Path.Widen(pen);

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


void Draw(Graphics graphics)
{
    using(Pen pen = new Pen(Color.Blue, 0))
        graphics.DrawPath(pen, Path);
}
Coincoin
Very good. ... indeed ...
Richard Hein
Very nice, thanks
johnc