Question

when using OnRender to draw something on the screen, is there any way to perform Hit Testing on the drawn graphics?

Sample Code

    protected override void OnRender(System.Windows.Media.DrawingContext drawingContext)
    {
        base.OnRender(drawingContext);

        drawingContext.DrawRectangle(Brushes.Black, null, new Rect(50, 50, 100, 100));
    }

Obviously one has no reference to the drawn Rectangle which would be necessary to perform hit testing or am I wrong about this? I know I can use DrawingVisual, I'm just curious if my understanding is correct, that using OnRender to draw something you can't perform any hit testing on the drawn things?

Was it helpful?

Solution

You understood right, there is no way do hittesting on the graphics you have drawn using the DrawingContext methods because they are not represented as objects in the visual tree.

OTHER TIPS

Unless I'm completely misunderstanding the OP's question, the answer is YES the hit test is based on what's drawn during OnRender. You can tell by overriding OnMouseEnter in the same class in which you override OnRender. In the override of OnMouseEnter try this:

var htr = VisualTreeHelper.HitTest(this, e.GetPosition(this));
if (htr != null)
     System.Diagnostics.Debug.WriteLine("It's a hit!");

Now, I notice you're still calling base.OnRender which suggests you are trying to draw ON TOP of what the Visual would otherwise render. What you won't be able to do, then, is distinguish a hit on what the parent class rendered from what you rendered in your override. However if you need that, just keep your drawing contained to its own Visual.

My quick solution. Its not ideal in many ways but it works for me as a start. Can easily be refactored for performance if that becomes necessary.

    /// <summary>
    /// Provides basic hit testing
    /// </summary>
    public class RadarHitTestUtility
    {
        public Dictionary<Tuple<int,int>, HitContainer> HitStorage = new Dictionary<Tuple<int, int>, HitContainer>();

        public void AddEllipse(RadarObject radarObject, Point point, double x, double y)
        {
            // Geometry used for hit testing
            var elipse = new EllipseGeometry
            {
                Center = point,
                RadiusX = x,
                RadiusY = y,
            };

            var key = new Tuple<int, int>((int)point.X, (int)point.Y);

            var hit = new HitContainer
            {
                Geometry = elipse,
                Bounds = elipse.Bounds,
                RadarObject = radarObject
            };

            if(!HitStorage.ContainsKey(key))
                HitStorage.Add(key, hit);
        }

        /// <summary>
        /// Gets first radar object whose center point is within distance of point.
        /// </summary>
        public HitContainer GetSimpleHit(Point point, int distance = 5)
        {
            foreach (var hit in HitStorage)
            {
                if (Math.Abs(hit.Key.Item1 - point.X) <= distance && Math.Abs(hit.Key.Item2 - point.Y) <= distance)
                {
                    hit.Value.Intersection = IntersectionDetail.NotCalculated;
                    return hit.Value;
                }
            }
            return default(HitContainer);
        }

        /// <summary>
        /// Gets first radar object that contains point
        /// </summary>
        public HitContainer GetHit(Point point)
        {
            var PreCheckDistance = 50;

            foreach (var hit in HitStorage)
            {
                if (Math.Abs(hit.Key.Item1 - point.X) <= PreCheckDistance && Math.Abs(hit.Key.Item2 - point.Y) <= PreCheckDistance)
                {
                    if (hit.Value.Geometry.FillContains(point))
                    {
                        hit.Value.Intersection = IntersectionDetail.FullyInside;
                        return hit.Value;
                    }                    
                }
            }
            return default(HitContainer);
        }

        /// <summary>
        /// Gets first radar object that intersects with geometry
        /// </summary>
        public HitContainer GetHit(Geometry geometry)
        {
            var PreCheckDistance = 50;
            var GeometryCheckTolerance = 1;
            var center = geometry.Bounds.Center();

            foreach (var hit in HitStorage)
            {
                if (Math.Abs(hit.Key.Item1 - center.X) <= PreCheckDistance && Math.Abs(hit.Key.Item2 - center.Y) <= PreCheckDistance)
                {
                    var result = hit.Value.Geometry.FillContainsWithDetail(geometry, GeometryCheckTolerance, ToleranceType.Absolute);
                    if (result != IntersectionDetail.Empty)
                    {
                        hit.Value.Intersection = result;
                        return hit.Value;
                    }
                }
            }
            return default(HitContainer);
        }

        public class HitContainer
        {
            public RadarObject RadarObject { get; set; }
            public Geometry Geometry { get; set; }
            public Rect Bounds { get; set; }
            public IntersectionDetail Intersection { get; set; }
        }

        public void Clear()
        {
            HitStorage.Clear();
        }
    }

Declare an instance in your control

public RadarHitTestUtility HitTester = new RadarHitTestUtility();

You'll need to call Clear() at some point so that only current objects exist in the hit test collection.

HitTester.Clear();

In OnRender() when you want a draw call to be recorded for hit testing purposes you can record it with AddEllipse()

HitTester.AddEllipse(radarObject, radarObject.Point, actorRadius, actorRadius);

Here's how i am calling for the hit check

    private void MouseDownHandler(object sender, MouseButtonEventArgs e)
    {
        IsMouseDown = true;
        Cursor = Cursors.Hand;
        DragInitialPosition = Mouse.GetPosition(this);
        DragInitialPanOffset = CanvasData.PanOffset;

        var hit = FindElementUnderClick(sender, e);
        if (hit != null)
        {
            SelectedItem = hit.RadarObject.Actor;
        }            
    }

    private RadarHitTestUtility.HitContainer FindElementUnderClick(object sender, MouseEventArgs e)
    {
        var position = e.GetPosition((UIElement)sender);            
        return HitTester.GetHit(position);
    }

If you want to be able to test shapes other than an ellipse you would just add the appropriate recording method into RadarHitTestUtility and call it, creating a different kind of Geometry to test against.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top