VisualTreeHelper.HitTest сообщает о попадании, даже когда прямоугольник находится далеко от основных фигур - PullRequest
2 голосов
/ 30 сентября 2019

Я пытаюсь реализовать выбор с помощью резиновой ленты объектов пути WPF на холсте. К сожалению, мое использование VisualTreeHelper.HitTest с геометрией прямоугольника не работает, как я ожидаю.

Я ожидаю получить удар, только когда какая-то часть моего прямоугольника с резинкой пересекает траекторию линии. Но с прямоугольником, каждый раз, когда мой прямоугольник находится где-нибудь слева или над линией, я получаю удар, даже если я не нахожусь рядом с линией или даже с ее ограничительной рамкой.

IsЕсть ли способ обойти это или что-то очевидное, что я делаю не так?

Я написал простое приложение, чтобы продемонстрировать проблему. Это одна строка и метка. Если мой вызов VisualTreeHelper.HitTest (с использованием прямоугольника с резинкой) обнаруживает, что он находится над формой, я устанавливаю метку внизу для Visible. В противном случае метка свернута.

Здесь я нахожусь прямо над линией и, как я ожидаю, обнаруживает попадание. Это хорошо.

Successful hit as expected

Здесь я ниже линии и никакого удара нет. Это также хорошо

Below line, no hit

Но когда я нахожусь где-нибудь слева или над линией, независимо от того, как далеко, я получаю удар

enter image description here

Вот окно тестового приложения:

<Window x:Class="WpfApp1.MainWindow"


        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        xmlns:po="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options"
        Title="MainWindow" Height="500" Width="525">
    <Window.Resources>

        <LineGeometry x:Key="LineGeo" StartPoint="50, 100" EndPoint="200, 75"/>
    </Window.Resources>
    <Canvas 
        x:Name="MyCanvas"
        Background="Yellow"
        MouseLeftButtonDown="MyCanvas_OnMouseLeftButtonDown"
        MouseMove="MyCanvas_OnMouseMove"
        MouseLeftButtonUp="MyCanvas_OnMouseLeftButtonUp"
        >

        <!-- The line I hit-test -->

        <Path x:Name="MyLine" Data="{StaticResource LineGeo}" 
              Stroke="Black" StrokeThickness="5" Tag="1234" />

        <!-- This label's is hidden by default and only shows up when code-behind sets it to Visible -->

        <Label x:Name="MyLabel"  Canvas.Left="100"  Canvas.Top="200" 
               Content="HIT DETECTED!!!" FontSize="25"  FontWeight="Bold" 
               Visibility="{x:Static Visibility.Collapsed}"/>

    </Canvas>
</Window>

А вот обработчики кода мыши с кодом HitTest

using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        public MainWindow() => InitializeComponent();

        private Point _startPosition;
        Path _path;
        private RectangleGeometry _rectGeo;
        private static readonly SolidColorBrush _brush = new SolidColorBrush(Colors.BlueViolet) { Opacity=0.3 };


        private void MyCanvas_OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            e.Handled = true;
            MyCanvas.CaptureMouse();
            _startPosition = e.GetPosition(MyCanvas);

            // Create the visible selection rect and add it to the canvas

            _rectGeo = new RectangleGeometry();
            _rectGeo.Rect = new Rect(_startPosition, _startPosition);
            _path = new Path()
            {
                Data = _rectGeo, 
                Fill =_brush,
                StrokeThickness = 0,
                IsHitTestVisible = false
            };

            MyCanvas.Children.Add(_path);
        }

        private void MyCanvas_OnMouseMove(object sender, MouseEventArgs e)
        {
            // Sanity check

            if (e.MouseDevice.LeftButton != MouseButtonState.Pressed ||
                null == _path ||
                !MyCanvas.IsMouseCaptured)
            {
                return;
            }  

            e.Handled = true;

            // Get the second position for the rect geometry

            var curPos    = e.GetPosition(MyCanvas);
            var rect      = new Rect(_startPosition, curPos);
            _rectGeo.Rect = rect;
            _path.Data    = _rectGeo;

            // This is set up like a loop because my real production code is looking
            // for many shapes.

            var paths          = new List<Path>();
            var htp            = new GeometryHitTestParameters(_rectGeo);
            var resultCallback = new HitTestResultCallback(r => HitTestResultBehavior.Continue);
            var filterCallback = new HitTestFilterCallback(
                el =>
                {
                    // Filter accepts any object of type Path.  There should be just one

                    if (el is Path s && s.Tag != null)
                        paths.Add(s);

                    return HitTestFilterBehavior.Continue;

                });

            VisualTreeHelper.HitTest(MyCanvas, filterCallback, resultCallback, htp);

            // Set the label visibility based on whether or not we hit the line

            var line  = paths.FirstOrDefault();
            MyLabel.Visibility =  ReferenceEquals(line, MyLine) ? Visibility.Visible : Visibility.Collapsed;
        }
        private void MyCanvas_OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (null == _path)
                return; 

            e.Handled = true;
            MyLabel.Visibility = Visibility.Collapsed;
            MyCanvas.Children.Remove(_path);
            _path = null;

            if (MyCanvas.IsMouseCaptured)
                MyCanvas.ReleaseMouseCapture();

        }
    }
}

1 Ответ

1 голос
/ 30 сентября 2019

Проблема в вашем коде заключается в том, что вы неправильно используете обратные вызовы теста на попадание. Фильтр обратного вызова используется для исключения объектов из проверки попадания. И это обратный вызов результата, который предоставляет вам информацию о том, что на самом деле тестируется как попадание.

Но по какой-то причине вы используете обратный вызов фильтра для записи результатов тестирования попадания. Это приводит к бессмысленным результатам. Честно говоря, это просто совпадение, что вообще существует какая-либо связь между перетаскиваемым прямоугольником и объектом, проверенным на удар. Это просто артефакт оптимизации тестирования на попадание, которую имеет WPF.

Вот реализации для ваших обратных вызовов, которые будут работать правильно:

var resultCallback = new HitTestResultCallback(
    r =>
    {
        if (r is GeometryHitTestResult g &&
            g.IntersectionDetail != IntersectionDetail.Empty &&
            g.IntersectionDetail != IntersectionDetail.NotCalculated &&
            g.VisualHit is Path p)
        {
            paths.Add(p);
        }

        return HitTestResultBehavior.Continue;
    });
var filterCallback = new HitTestFilterCallback(
    el =>
    {
        // Filter accepts any object of type Path.  There should be just one
        return string.IsNullOrEmpty((string)(el as Path)?.Tag) ?
            HitTestFilterBehavior.ContinueSkipSelf : HitTestFilterBehavior.Continue;

    });

В приведенном выше примере обратный вызов результата проверяет, чтопересечение было вычислено и не является пустым, и, если это так, проверяет тип попадания объекта и, если это ожидаемый объект Path, добавляет его в список.

Обратный вызов фильтра просто исключаетлюбой объект, который не является Path объектом. Обратите внимание, что в теории, учитывая эту реализацию, обратный вызов результата может просто привести объект VisualHit вместо использования is. В основном это вопрос личных предпочтений, каким образом это сделать.

...