可视化层中的命中测试

本主题概述可视化层提供的命中测试功能。 通过命中测试支持,可以确定几何或点值是否落在 Visual 的呈现内容内,从而可以实现用户界面行为,例如用于选择多个对象的选择矩形。

命中测试方案

UIElement 类提供 InputHitTest 方法,允许使用给定的坐标值针对元素进行命中测试。 在许多情况下,InputHitTest 方法为实现元素的命中测试提供了所需功能。 但是,有多种方案可能需要在可视化层上实现命中测试。

  • 针对非 UIElement 对象进行命中测试:适用于对非 UIElement 对象(如 DrawingVisual 或图形对象)进行命中测试。

  • 使用几何进行命中测试:适用于需要使用几何对象而不是点的坐标值进行命中测试。

  • 针对多个对象进行命中测试:适用于需要针对多个对象(如重叠的对象)进行命中测试。 可以获取与几何或点相交的所有视觉对象的结果,而不仅仅是第一个视觉对象的结果。

  • 忽略 UIElement 命中测试策略:适用于需要忽略 UIElement 命中测试策略,该策略将元素是否已禁用或不可见等因素考虑在内。

注意

有关演示在可视化层上进行命中测试的完整代码示例,请参阅使用 DrawingVisuals 进行命中测试示例使用 Win32 互操作进行命中测试示例

命中测试支持

VisualTreeHelper 类中 HitTest 方法的用途是确定几何或点坐标值是否在给定对象的呈现内容内,如控件或图形元素。 例如,可以使用命中测试确定对象边框内的鼠标单击是否落在圆形的几何内。 还可以选择重写命中测试的默认实现,以执行自己的自定义命中测试计算。

下图显示非矩形对象的区域与其边框之间的关系。

Diagram of valid hit test region
有效命中测试区域示意图

命中测试和 Z 顺序

Windows Presentation Foundation (WPF) 可视化层支持针对点或几何下的所有对象(而不仅仅是最顶层对象)进行命中测试。 结果按 z 顺序返回。 但是,作为参数传递到 HitTest 方法的视觉对象确定将对可视化树的哪个部分进行命中测试。 可以针对整个可视化树或它的任意部分进行命中测试。

在下图中,圆形对象在正方形和三角形对象之上。 如果只希望对其 z 顺序值为最顶层的视觉对象进行命中测试,则可以设置可视化命中测试枚举,使其在第一个项之后从 HitTestResultCallback 返回 Stop 以停止命中测试遍历。

Diagram of the z-order of a visual tree
可视化树的 z 顺序示意图

如果要枚举特定点或几何下的所有视觉对象,请从 HitTestResultCallback 返回 Continue。 这意味着可以为其他对象之下的视觉对象进行命中测试,即使它们被完全遮挡也是如此。 有关详细信息,请参阅“使用命中测试结果回叫”部分中的示例代码。

注意

还可以对透明的视觉对象进行命中测试。

使用默认命中测试

通过使用 HitTest 方法指定视觉对象和测试所针对的点坐标值,可以确定某个点是否在视觉对象的几何内。 视觉对象参数为命中测试搜索确定可视化树中的起始点。 如果在可视化树中发现了其几何包含该坐标的视觉对象,则将它设置为 HitTestResult 对象的 VisualHit 属性。 然后从 HitTest 方法返回 HitTestResult。 如果要执行命中测试的可视化子树中不包含该点,则 HitTest 返回 null

注意

默认命中测试始终返回 z 顺序中最顶层的对象。 为了标识所有视觉对象(甚至是被部分或完全遮挡的视觉对象),请使用命中测试结果回叫。

作为 HitTest 方法的点参数传递的坐标值必须相对于命中测试所针对的视觉对象的坐标空间。 例如,如果在父级坐标空间的 (100, 100) 处定义了嵌套可视化对象,则对位于 (0, 0) 的子视觉对象进行命中测试等效于对父级坐标空间的 (100, 100) 处的子视觉对象进行命中测试。

以下代码显示如何为 UIElement 对象设置鼠标事件处理程序,该对象用于捕获用于命中测试的事件。

// 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

可视化树如何影响命中测试

可视化树中的起始点确定在对象的命中测试枚举期间返回哪些对象。 如果要对多个对象进行命中测试,在可视化树中用作起始点的视觉对象必须是所有相关对象的公共上级。 例如,如果希望对以下关系图中的按钮元素和绘图视觉对象进行命中测试,必须将可视化树中的起始点设置为两者的公共上级。 在这种情况下,画布元素是按钮元素和绘图视觉对象的公共上级。

Diagram of a visual tree hierarchy
可视化树的层次结构示意图

注意

IsHitTestVisible 属性获取或设置一个值,该值声明 UIElement 派生的对象是否可以作为其呈现内容某个部分的命中测试结果返回。 这使用户可以选择性地更改可视化树,以确定命中测试涉及哪些视觉对象。

使用命中测试结果回叫

可以在可视化树中枚举其几何包含特定坐标值的所有视觉对象。 这使用户可以标识所有视觉对象(甚至是被其他视觉对象部分或完全遮挡的视觉对象)。 若要枚举可视化树中的视觉对象,请将 HitTest 方法与命中测试回叫函数结合使用。 当指定的坐标值包含在视觉对象中时,系统会调用命中测试回叫函数。

在命中测试结果枚举期间,不应执行任何修改可视化树的操作。 在遍历可视化树的过程中,在可视化树中添加或删除对象可能会导致不可预知的行为。 在 HitTest 方法返回后,可以安全地修改可视化树。 用户可能需要提供一个数据结构(如 ArrayList),以在命中测试结果枚举期间存储值。

// 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

命中测试回叫方法定义在可视化树中的特定视觉对象上标识命中测试时要执行的操作。 执行操作后,返回一个 HitTestResultBehavior 值,该值确定是否继续枚举任何其他视觉对象。

// 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

注意

命中视觉对象的枚举顺序为 z 顺序。 位于最顶层 z 顺序级别上的视觉对象是第一个枚举的对象。 所有其他视觉对象按递减的 z 顺序级别进行枚举。 此枚举顺序对应于视觉对象的呈现顺序。

可通过返回 Stop 在命中测试回叫函数中随时停止对视觉对象的枚举。

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

使用命中测试筛选器回叫

可使用可选的命中测试筛选器来限制传递给命中测试结果的对象。 这样一来,你便可以在处理命中测试结果时忽略可视化树中的无关部分。 若要实现命中测试筛选器,请定义一个命中测试筛选器回叫函数,并在调用 HitTest 方法时将它作为参数值传递。

// 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

如果不希望提供可选的命中测试筛选器回叫函数,请为 HitTest 方法传递一个 null 值作为其参数。

// 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
修剪可视化树

借助命中测试筛选器回叫函数,可以允许枚举呈现内容包含指定坐标的所有视觉对象。 但是,你可能要忽略不希望在命中测试结果回调叫函数中处理的可视化树的某些分支。 命中测试筛选器回叫函数的返回值确定视觉对象的枚举应采用的操作类型。 例如,如果返回值 ContinueSkipSelfAndChildren,则可以从命中测试结果枚举中删除当前视觉对象及其子级。 这意味着,命中测试结果回叫函数不会在其枚举中看到这些对象。 修剪对象的可视化树会减少命中测试结果枚举传递期间的处理量。 在以下代码示例中,筛选器会跳过标签及其后代,并对其他所有内容进行命中测试。

// 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

注意

在未调用命中测试结果回叫的情况下,有时会调用命中测试筛选器回叫。

重写默认命中测试

可以通过重写 HitTestCore 方法重写视觉对象的默认命中测试支持。 这意味着,在调用 HitTest 方法时,将调用 HitTestCore 的替代实现。 当命中测试落在视觉对象的边框内时,将调用重写的方法,即使坐标落在视觉对象呈现内容之外也是如此。

// 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

有时你可能希望针对视觉对象的边框和呈现内容进行命中测试。 通过在 HitTestCore 方法中使用 PointHitTestParameters 参数作为基方法 HitTestCore 的参数,可以基于视觉对象边框的命中来执行操作,然后针对视觉对象的呈现内容执行第二次命中测试。

// 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

另请参阅