2

Say i want to try to make a straight line albeit with any angle

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);
    }
}

This line has been made on a custom control.

Now how can i calculate the exact pixels on which the line has been made so i can implement a hit test with MouseMove.

Win Coder
  • 6,628
  • 11
  • 54
  • 81
  • All of this sounds a lot like you're `reinventing the wheel`. Forget winforms use WPF which already does this all. – Federico Berasategui Jun 28 '13 at 18:15
  • Are you trying to see if the mouse is within X pixels of this random line? – Tombala Jun 28 '13 at 18:15
  • @Tombala Yes exactly, I can handle everything else i just need to know how to calculate the pixels in between the 2 points – Win Coder Jun 28 '13 at 18:17
  • If you are looking for an exact hit, you could check to see if the color of the pixel is `Color.Red`. – mbeckish Jun 28 '13 at 18:18
  • @mbeckish great idea. Any starting Points ? – Win Coder Jun 28 '13 at 18:21
  • 1
    @WinCoder - It depends on what abstraction of the "line" you are concered with. For example, you might want to consider the line to be a "mathematical" line, with 0 width, and want to know if the selected pixel is intersected by this theoretical line. Or, you might be concerned with the actual pixels whose colors were changed by the `e.Graphics.DrawLine(pen, start, end)` command, which will depend upon settings such as anti-aliasing, etc. – mbeckish Jun 28 '13 at 18:22
  • @mbeckish Nahh not mathematical but the actual pixels whose color is red – Win Coder Jun 28 '13 at 18:23
  • @WinCoder - Pure Color.Red, or some shade of near red because of antialiasing pixels to blend with other nearby pixels? – mbeckish Jun 28 '13 at 18:41
  • @mbeckish Color.Red will suffice i just need some way to detect the pixels for the hit test. BTW i searched a bit on detecting pixel color and boy it does involve some serious win32API – Win Coder Jun 28 '13 at 18:47
  • @WinCoder - You can just use [GetPixel](http://msdn.microsoft.com/en-us/library/system.drawing.bitmap.getpixel.aspx) – mbeckish Jun 28 '13 at 18:49
  • @mbeckish yes i also searched a bit on this one too. But here's the problem. What kind of input can i give to the bitmap. A Whole ScreenShot of the form ? now that wouldn't be so practical would it – Win Coder Jun 28 '13 at 18:53
  • 1
    You should use math for this, calculate the normal of your point for the line. Is the normals length less than 1, or whatever your desired precision is, we call it a collision. Or super simple, calculate both distances (from mouseposition to each point of the line). If the sum of those distances is close to the line-length, you have a collision aswell – CSharpie Jun 28 '13 at 18:54
  • @WinCoder - If you are going the route of detecting the color of the selected pixel, then this is a possible duplicate of [How do I get the colour of a pixel at X,Y using c#?](http://stackoverflow.com/questions/753132/how-do-i-get-the-colour-of-a-pixel-at-x-y-using-c) – mbeckish Jun 28 '13 at 18:55
  • @CSharpie I don't want t seem imposing but a little bit of elaboration please. – Win Coder Jun 28 '13 at 18:57
  • http://hub.tutsplus.com/tutorials/predicting-collision-points-with-math-in-as3--active-11218?request_uri=%2Ftutorials%2Factionscript%2Fpredicting-collision-points-with-math-in-as3%2F – CSharpie Jun 28 '13 at 19:06
  • I really fail to see the point to hittest exact pixels. Ive been doing stuff like this for more than 15 years and nobody ever asked for that. –  Jun 28 '13 at 19:08
  • @CSharpie Wow that looks like a great page thanks. – Win Coder Jun 28 '13 at 19:09
  • @jdv-JandeVaan apologies if i seem ambiguous. I don't want to hit test the exact pixel. Mouse Cursor Close Enough would suffice. – Win Coder Jun 28 '13 at 19:24
  • @Wincoder please look at the example I left you in your previous question about this. – Federico Berasategui Jun 29 '13 at 04:52

5 Answers5

1

There are Win32 calls for enumerating the pixels of a line that would be drawn using GDI calls. I believe this is the best technique for what you're trying to accomplish. See LineDDA and its associated callback LineDDAProc.

Here's how you would use it from C#. Note that the end point is not included in the output, as per the documentation of 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);
Michael Gunter
  • 12,528
  • 1
  • 24
  • 58
  • Well thks for the headsup but i am very new even to C# let alone Win32, and as such i am really clueless on this stuff. Anyways thanks gave me another way which i could use if everything else dries up. – Win Coder Jun 28 '13 at 18:56
  • Consider accepting this as an answer, or adjusting your question's title to better indicate this is a hit-test question (and then accept someone else's answer). This will help someone else find the answer later. – Michael Gunter Jun 28 '13 at 19:36
0

You should look at this question which provides some code to calculate the distance from a point to a given line segment with a begin and end point. It provides C++ and Javascript versions which are both very close to C#. I would add a method to your Line class that uses that code:

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.
    }
}

Then check the distance to be, say, less than 2 pixels.

Community
  • 1
  • 1
Tombala
  • 1,660
  • 9
  • 11
0

If you really want to do it like this, draw your control twice:

  1. once to screen,
  2. once to an offscreen buffer.

The obvious way is to make a buffer the same size as your control's client rectangle.

On the offscreen, you can turn off antialiasing so you can read the color values exactly as you wrote them. Now you can simply read from the bitmap. If you need to hit test multiple lines, put the index value in the color.

0

There are more complicated ways of doing this, but the simple way is just to process click events for your custom control. In other words, add a handler for the MouseClick event that is raised by the Control base class. This way, Windows does all of the hit-testing for you.

If the user clicks anywhere on the control, the MouseClick event will be raised and you can process it however you want. Otherwise, no event is raised. The epitome of simplicity.

In the MouseClick event handler, you'll get a point (e.Location) in client coordinates, meaning that the location is relative to the upper-left hand corner of the client control.

For testing purposes, I just added a Label control to an empty form, turned off AutoSize, and set the BackColor to red. Then I made it look like a line, and added a handler for the MouseClick event. The handler looks like this:

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);
}

This simplistic method of hit testing relies on the fact that the physical boundaries of your control as far as Windows is concerned are the same as its logical boundaries. So to make it work with your custom control, you'll need to ensure that you're setting its Size property to its actual logical dimensions (i.e., the width and thickness of the line).

Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
  • That's the real problem so far i have been unable to say make a diagonal component for a diagonal line. To complicate matters users can make any arbritrary line meaning that the shape of control not only has to generated at runtime but also has to arbritrary as well. – Win Coder Jun 28 '13 at 21:47
0

If you just want to see if the mouse is near a line segment, you don't need to know exactly where the pixels are - you just need to know if they are logically within a certain distance.

Here's a little class I knocked together. It just uses the normal formula for a line y = mx+c to calculate if any particular point is within a certain distance (tolerance) of the line.

Given two points, p1 and p2 that are the coords of the endpoints of a line you want to hit-test, you would initialise it like this:

var hitTest = new LineIntersectionChecker(p1, p2);

Then check if another point, p is on the line like this:

if (hitTest.IsOnLine(p))
    ...

The class implementation:

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);
    }
}

You use it by instantiating it with the points at either end of the line that you want to hit-test, and then call IsOnLine(p) for each point you want to check against the line.

You would get the points to check from MouseMove or MouseDown messages.

Note that you can set a different tolerance in the constructor. I defaulted it to 1 because "within 1 pixel" seems a reasonable default.

Here's the code I tested it with:

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));
}
Matthew Watson
  • 104,400
  • 10
  • 158
  • 276