Muokkaa

Jaa


Hit Testing in the Visual Layer

This topic provides an overview of hit testing functionality provided by the visual layer. Hit testing support allows you to determine whether a geometry or point value falls within the rendered content of a Visual, allowing you to implement user interface behavior such as a selection rectangle to select multiple objects.

Hit Testing Scenarios

The UIElement class provides the InputHitTest method, which allows you to hit test against an element using a given coordinate value. In many cases, the InputHitTest method provides the desired functionality for implementing hit testing of elements. However, there are several scenarios in which you may need to implement hit testing at the visual layer.

  • Hit testing against non-UIElement objects: This applies if you are hit testing non-UIElement objects, such as DrawingVisual or graphics objects.

  • Hit testing using a geometry: This applies if you need to hit test using a geometry object rather than the coordinate value of a point.

  • Hit testing against multiple objects: This applies when you need to hit test against multiple objects, such as overlapping objects. You can get results for all visuals intersecting a geometry or point, not just the first one.

  • Ignoring UIElement hit testing policy: This applies when you need to ignore the UIElement hit testing policy, which takes into consideration such factors as whether an element is disabled or invisible.

Note

For a complete code sample illustrating hit testing at the visual layer, see Hit Test Using DrawingVisuals Sample and Hit Test with Win32 Interoperation Sample.

Hit Testing Support

The purpose of the HitTest methods in the VisualTreeHelper class is to determine whether a geometry or point coordinate value is within the rendered content of a given object, such as a control or graphic element. For example, you could use hit testing to determine whether a mouse click within the bounding rectangle of an object falls within the geometry of a circle. You can also choose to override the default implementation of hit testing to perform your own custom hit test calculations.

The following illustration shows the relationship between a non-rectangular object's region and its bounding rectangle.

Diagram of valid hit test region
Diagram of valid hit test region

Hit Testing and Z-Order

The Windows Presentation Foundation (WPF) visual layer supports hit testing against all objects under a point or geometry, not just the top-most object. Results are returned in z-order. However, the visual object that you pass as the parameter to the HitTest method determines which portion of the visual tree that will be hit test. You can hit test against the entire visual tree, or any portion of it.

In the following illustration, the circle object is on top of both the square and triangle objects. If you are only interested in hit testing the visual object whose z-order value is top-most, you can set the visual hit test enumeration to return Stop from the HitTestResultCallback to stop the hit test traversal after the first item.

Diagram of the z-order of a visual tree
Diagram of the z-order of a visual tree

If you want to enumerate all visual objects under a specific point or geometry, return Continue from the HitTestResultCallback. This means you can hit test for visual objects that are beneath other objects, even if they are wholly obscured. See the sample code in the section "Using a Hit Test Results Callback" for more information.

Note

A visual object that is transparent can also be hit test.

Using Default Hit Testing

You can identify whether a point is within the geometry of a visual object, by using the HitTest method to specify a visual object and a point coordinate value to test against. The visual object parameter identifies the starting point in the visual tree for the hit test search. If a visual object is found in the visual tree whose geometry contains the coordinate, it is set to the VisualHit property of a HitTestResult object. The HitTestResult is then returned from the HitTest method. If the point is not contained with the visual sub-tree you are hit testing, HitTest returns null.

Note

Default hit testing always returns the top-most object in the z-order. In order to identify all visual objects, even those that may be partly or wholly obscured, use a hit test result callback.

The coordinate value you pass as the point parameter for the HitTest method has to be relative to the coordinate space of the visual object you are hit testing against. For example, if you have nested visual objects defined at (100, 100) in the parent's coordinate space, then hit testing a child visual at (0, 0) is equivalent to hit testing at (100, 100) in the parent's coordinate space.

The following code shows how to set up mouse event handlers for a UIElement object that is used to capture events used for hit testing.

// Respond to the left mouse button down event by initiating the hit test.
private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    // Retrieve the coordinate of the mouse position.
    Point pt = e.GetPosition((UIElement)sender);

    // Perform the hit test against a given portion of the visual object tree.
    HitTestResult result = VisualTreeHelper.HitTest(myCanvas, pt);

    if (result != null)
    {
        // Perform action on hit visual object.
    }
}
' Respond to the left mouse button down event by initiating the hit test.
Private Overloads Sub OnMouseLeftButtonDown(ByVal sender As Object, ByVal e As MouseButtonEventArgs)
    ' Retrieve the coordinate of the mouse position.
    Dim pt As Point = e.GetPosition(CType(sender, UIElement))

    ' Perform the hit test against a given portion of the visual object tree.
    Dim result As HitTestResult = VisualTreeHelper.HitTest(myCanvas, pt)

    If result IsNot Nothing Then
        ' Perform action on hit visual object.
    End If
End Sub

How the Visual Tree Affects Hit Testing

The starting point in the visual tree determines which objects are returned during the hit test enumeration of objects. If you have multiple objects you want to hit test, the visual object used as the starting point in the visual tree must be the common ancestor of all objects of interest. For example, if you were interested in hit testing both the button element and drawing visual in the following diagram, you would have to set the starting point in the visual tree to the common ancestor of both. In this case, the canvas element is the common ancestor of both the button element and the drawing visual.

Diagram of a visual tree hierarchy
Diagram of a visual tree hierarchy

Note

The IsHitTestVisible property gets or sets a value that declares whether a UIElement-derived object can possibly be returned as a hit test result from some portion of its rendered content. This allows you to selectively alter the visual tree to determine which visual objects are involved in a hit test.

Using a Hit Test Result Callback

You can enumerate all visual objects in a visual tree whose geometry contains a specified coordinate value. This allows you to identify all visual objects, even those that may be partly or wholly obscured by other visual objects. To enumerate visual objects in a visual tree use the HitTest method with a hit test callback function. The hit test callback function is called by the system when the coordinate value you specify is contained in a visual object.

During the hit test results enumeration, you should not perform any operation that modifies the visual tree. Adding or removing an object from the visual tree while it is being traversed can result in unpredictable behavior. You can safely modify the visual tree after the HitTest method returns. You may want to provide a data structure, such as an ArrayList, to store values during the hit test results enumeration.

// Respond to the right mouse button down event by setting up a hit test results callback.
private void OnMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
    // Retrieve the coordinate of the mouse position.
    Point pt = e.GetPosition((UIElement)sender);

    // Clear the contents of the list used for hit test results.
    hitResultsList.Clear();

    // Set up a callback to receive the hit test result enumeration.
    VisualTreeHelper.HitTest(myCanvas, null,
        new HitTestResultCallback(MyHitTestResult),
        new PointHitTestParameters(pt));

    // Perform actions on the hit test results list.
    if (hitResultsList.Count > 0)
    {
        Console.WriteLine("Number of Visuals Hit: " + hitResultsList.Count);
    }
}
' Respond to the right mouse button down event by setting up a hit test results callback.
Private Overloads Sub OnMouseRightButtonDown(ByVal sender As Object, ByVal e As MouseButtonEventArgs)
    ' Retrieve the coordinate of the mouse position.
    Dim pt As Point = e.GetPosition(CType(sender, UIElement))

    ' Clear the contents of the list used for hit test results.
    hitResultsList.Clear()

    ' Set up a callback to receive the hit test result enumeration.
    VisualTreeHelper.HitTest(myCanvas, Nothing, New HitTestResultCallback(AddressOf MyHitTestResult), New PointHitTestParameters(pt))

    ' Perform actions on the hit test results list.
    If hitResultsList.Count > 0 Then
        Console.WriteLine("Number of Visuals Hit: " & hitResultsList.Count)
    End If
End Sub

The hit test callback method defines the actions you perform when a hit test is identified on a particular visual object in the visual tree. After you perform the actions, you return a HitTestResultBehavior value that determines whether to continue the enumeration of any other visual objects or not.

// Return the result of the hit test to the callback.
public HitTestResultBehavior MyHitTestResult(HitTestResult result)
{
    // Add the hit test result to the list that will be processed after the enumeration.
    hitResultsList.Add(result.VisualHit);

    // Set the behavior to return visuals at all z-order levels.
    return HitTestResultBehavior.Continue;
}
' Return the result of the hit test to the callback.
Public Function MyHitTestResult(ByVal result As HitTestResult) As HitTestResultBehavior
    ' Add the hit test result to the list that will be processed after the enumeration.
    hitResultsList.Add(result.VisualHit)

    ' Set the behavior to return visuals at all z-order levels.
    Return HitTestResultBehavior.Continue
End Function

Note

The order of enumeration of hit visual objects is by z-order. The visual object at the top-most z-order level is the first object enumerated. Any other visual objects enumerated are at decreasing z-order level. This order of enumeration corresponds to the rendering order of the visuals.

You can stop the enumeration of visual objects at any time in the hit test callback function by returning Stop.

// Set the behavior to stop enumerating visuals.
return HitTestResultBehavior.Stop;
' Set the behavior to stop enumerating visuals.
Return HitTestResultBehavior.Stop

Using a Hit Test Filter Callback

You can use an optional hit test filter to restrict the objects that are passed on to the hit test results. This allows you to ignore parts of the visual tree that you are not interested in processing in your hit test results. To implement a hit test filter, you define a hit test filter callback function and pass it as a parameter value when you call the HitTest method.

// Respond to the mouse wheel event by setting up a hit test filter and results enumeration.
private void OnMouseWheel(object sender, MouseWheelEventArgs e)
{
    // Retrieve the coordinate of the mouse position.
    Point pt = e.GetPosition((UIElement)sender);

    // Clear the contents of the list used for hit test results.
    hitResultsList.Clear();

    // Set up a callback to receive the hit test result enumeration.
    VisualTreeHelper.HitTest(myCanvas,
                      new HitTestFilterCallback(MyHitTestFilter),
                      new HitTestResultCallback(MyHitTestResult),
                      new PointHitTestParameters(pt));

    // Perform actions on the hit test results list.
    if (hitResultsList.Count > 0)
    {
        ProcessHitTestResultsList();
    }
}
' Respond to the mouse wheel event by setting up a hit test filter and results enumeration.
Private Overloads Sub OnMouseWheel(ByVal sender As Object, ByVal e As MouseWheelEventArgs)
    ' Retrieve the coordinate of the mouse position.
    Dim pt As Point = e.GetPosition(CType(sender, UIElement))

    ' Clear the contents of the list used for hit test results.
    hitResultsList.Clear()

    ' Set up a callback to receive the hit test result enumeration.
    VisualTreeHelper.HitTest(myCanvas, New HitTestFilterCallback(AddressOf MyHitTestFilter), New HitTestResultCallback(AddressOf MyHitTestResult), New PointHitTestParameters(pt))

    ' Perform actions on the hit test results list.
    If hitResultsList.Count > 0 Then
        ProcessHitTestResultsList()
    End If
End Sub

If you do not want to supply the optional hit test filter callback function, pass a null value as its parameter for the HitTest method.

// Set up a callback to receive the hit test result enumeration,
// but no hit test filter enumeration.
VisualTreeHelper.HitTest(myCanvas,
                  null,  // No hit test filtering.
                  new HitTestResultCallback(MyHitTestResult),
                  new PointHitTestParameters(pt));
' Set up a callback to receive the hit test result enumeration,
' but no hit test filter enumeration.
VisualTreeHelper.HitTest(myCanvas, Nothing, New HitTestResultCallback(AddressOf MyHitTestResult), New PointHitTestParameters(pt)) ' No hit test filtering.

Pruning a visual tree using a hit test filter
Pruning a visual tree

The hit test filter callback function allows you to enumerate through all the visuals whose rendered content contains the coordinates you specify. However, you may want to ignore certain branches of the visual tree that you are not interested in processing in your hit test results callback function. The return value of the hit test filter callback function determines what type of action the enumeration of the visual objects should take. For example, if you return the value, ContinueSkipSelfAndChildren, you can remove the current visual object and its children from the hit test results enumeration. This means that the hit test results callback function will not see these objects in its enumeration. Pruning the visual tree of objects decreases the amount of processing during the hit test results enumeration pass. In the following code example, the filter skips labels and their descendants and hit tests everything else.

// Filter the hit test values for each object in the enumeration.
public HitTestFilterBehavior MyHitTestFilter(DependencyObject o)
{
    // Test for the object value you want to filter.
    if (o.GetType() == typeof(Label))
    {
        // Visual object and descendants are NOT part of hit test results enumeration.
        return HitTestFilterBehavior.ContinueSkipSelfAndChildren;
    }
    else
    {
        // Visual object is part of hit test results enumeration.
        return HitTestFilterBehavior.Continue;
    }
}
' Filter the hit test values for each object in the enumeration.
Public Function MyHitTestFilter(ByVal o As DependencyObject) As HitTestFilterBehavior
    ' Test for the object value you want to filter.
    If o.GetType() Is GetType(Label) Then
        ' Visual object and descendants are NOT part of hit test results enumeration.
        Return HitTestFilterBehavior.ContinueSkipSelfAndChildren
    Else
        ' Visual object is part of hit test results enumeration.
        Return HitTestFilterBehavior.Continue
    End If
End Function

Note

The hit test filter callback will sometimes be called in cases where the hit test results callback is not called.

Overriding Default Hit Testing

You can override a visual object’s default hit testing support by overriding the HitTestCore method. This means that when you invoke the HitTest method, your overridden implementation of HitTestCore is called. Your overridden method is called when a hit test falls within the bounding rectangle of the visual object, even if the coordinate falls outside the rendered content of the visual object.

// Override default hit test support in visual object.
protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
    Point pt = hitTestParameters.HitPoint;

    // Perform custom actions during the hit test processing,
    // which may include verifying that the point actually
    // falls within the rendered content of the visual.

    // Return hit on bounding rectangle of visual object.
    return new PointHitTestResult(this, pt);
}
' Override default hit test support in visual object.
Protected Overrides Overloads Function HitTestCore(ByVal hitTestParameters As PointHitTestParameters) As HitTestResult
    Dim pt As Point = hitTestParameters.HitPoint

    ' Perform custom actions during the hit test processing,
    ' which may include verifying that the point actually
    ' falls within the rendered content of the visual.

    ' Return hit on bounding rectangle of visual object.
    Return New PointHitTestResult(Me, pt)
End Function

There may be times when you want to hit test against both the bounding rectangle and the rendered content of a visual object. By using the PointHitTestParameters parameter value in your overridden HitTestCore method as the parameter to the base method HitTestCore, you can perform actions based on a hit of the bounding rectangle of a visual object, and then perform a second hit test against the rendered content of the visual object.

// Override default hit test support in visual object.
protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
    // Perform actions based on hit test of bounding rectangle.
    // ...

    // Return results of base class hit testing,
    // which only returns hit on the geometry of visual objects.
    return base.HitTestCore(hitTestParameters);
}
' Override default hit test support in visual object.
Protected Overrides Overloads Function HitTestCore(ByVal hitTestParameters As PointHitTestParameters) As HitTestResult
    ' Perform actions based on hit test of bounding rectangle.
    ' ...

    ' Return results of base class hit testing,
    ' which only returns hit on the geometry of visual objects.
    Return MyBase.HitTestCore(hitTestParameters)
End Function

See also