views:

454

answers:

3

I am responding to MouseLeftButtonDown events on elements added to a WPF canvas. It all works fine when clicked (i.e. the eventhandler fires off correctly), but it requires too much precision from the mouse pointer. You have to be perfectly on top of the circle to make it work. I need it to be a little more forgiving; maybe at least 1 or 2 pixles forgiving. The elements on the canvas are nice big circles (about the size of a quarter on the screen), so the circles themselves are not too small, but the StrokeWidth of each one is 1, so it is a thin line.

You can see a screenshot here: http://twitpic.com/1f2ci/full

Most graphics app aren't this picky about the mouse picking, so I want to give the user a familiar experience.

How can I make it a little more forgiving.

+3  A: 

You can hook up to the MouseLeftButtonDown event of your root layout object instead, and check which elements is in range of a click by doing this:

List<UIElement> hits = System.Windows.Media.VisualTreeHelper.FindElementsInHostCoordinates(Point, yourLayoutRootElement) as List<UIElement>;

http://msdn.microsoft.com/en-us/library/cc838402(VS.95).aspx

For the Point parameter, you can use the MouseEventArgs parameter e, and call its GetPosition method like this:

Point p = e.GetPosition(null)

I can't remember whether to use HitTest instead of the FindElementsInHostCoordinates. Try both.
http://msdn.microsoft.com/en-us/library/ms608752.aspx

You could create 4 Point objects from the mouse position to create a fake tolerence effect, and call either FindElementsInHostCoordinates or HitTest for all 4 points.

MartinHN
Well, you got me going. Note: FindElementsInHostCoordinates si for silverlight only, so I had to use HitTest. I also did your 4-point thing. Check it out on my answer post below. Thanks.
MattSlay
I'm using the now famous "4-point thing" in conjonction with the HitTest function and it works fine for me! Thanks!
SuperOli
+4  A: 

You might want to try to fill the circle with the Transparent colour to make the whole circle clickable...

If that fails, you can also draw helper circles on the same location as the other circles. Make the circle foreground colour Transparent, and make the thickness of the brush a few pixels wider for a more acceptable clickable region around the circle..

Hope this helps!

Arcturus
A: 

I think I've done it (with you help to get me started)...

First, I've moved the move event handling to the Canvas instead of each Ellipse. That's good and bad, from an OOP standpoint. At least when the mouse event handling is a responsibility of the HolePattern to set it on up each Hole (the ellipse that is the visual of the Hole), it is abstracted away so that any consumer of my HolePattern will get this functioanality automactically. However, by moving it to the main UI code, I now am dealing with my canvas mouse event at a higher level. But that's not all bad either. We could discuss this part for days.

The point is, I have designed a way to create a "margin of error" when picking something on a canvas with a mouse, and then reading the Hole that the selected Ellipse belongs to, and then I can read the HolePattern that the Hole belongs to, and my entire UI (ListView, textboxes, gridview fo coordinates) are ALL updated by the existing XAML binding, and the Canvas is updated with one call to an existing method to regenerate the canvas.

To be honest, I can't believe I've figured all this out (with your help and others too, of course). It is such a cool feeling to have the vision of this this and see it come to be.

Check out the main code here:

void canvas1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    int ClickMargin = 2;
    Point ClickedPoint = e.GetPosition(canvas1);
    Point p1 = new Point(ClickedPoint.X - ClickMargin, ClickedPoint.Y - ClickMargin);
    Point p2 = new Point(ClickedPoint.X - ClickMargin, ClickedPoint.Y + ClickMargin);
    Point p3 = new Point(ClickedPoint.X + ClickMargin, ClickedPoint.Y + ClickMargin);
    Point p4 = new Point(ClickedPoint.X + ClickMargin, ClickedPoint.Y - ClickMargin);
    var PointPickList = new Collection<Point>();
    PointPickList.Add(ClickedPoint);
    PointPickList.Add(p1);
    PointPickList.Add(p2);
    PointPickList.Add(p3);
    PointPickList.Add(p4);

    foreach (Point p in PointPickList)
    {
        HitTestResult SelectedCanvasItem = System.Windows.Media.VisualTreeHelper.HitTest(canvas1, p);
        if (SelectedCanvasItem.VisualHit.GetType() == typeof(Ellipse))
        {
            var SelectedEllipseTag = SelectedCanvasItem.VisualHit.GetValue(Ellipse.TagProperty);
            if (SelectedEllipseTag!=null &&  SelectedEllipseTag.GetType().BaseType == typeof(Hole))
            {
                Hole SelectedHole = (Hole)SelectedEllipseTag;
                SetActivePattern(SelectedHole.ParentPattern);
                SelectedHole.ParentPattern.CurrentHole = SelectedHole;

            }
        }
    }
}
MattSlay
That's cool. I guess you could also place a transparent Rectangle on top of the circle, and hook up for its MouseLeftButtonDown event. But I think it makes it more complicated than your solution.
MartinHN
Dude! Thanks for the encouraging words. Vote up my post if you think it is worth it.
MattSlay