问题
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
.
回答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);
回答2:
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.
回答3:
If you really want to do it like this, draw your control twice:
- once to screen,
- 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.
回答4:
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).
回答5:
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));
}
来源:https://stackoverflow.com/questions/17371111/calculating-exact-pixels-for-a-line