Хит-тестирование в WPF для предметов неправильной формы - PullRequest
0 голосов
/ 12 сентября 2018

У меня есть элемент неправильной формы (форма линии), содержащийся в производном от ContentControl классе («ShapeItem»).Я оформляю его с помощью пользовательского курсора и обрабатываю щелчки мышью в классе ShapeItem.

К сожалению, WPF считает, что мышь находится «над» моим элементом, если она находится где-нибудь в прямоугольной ограничительной рамке ContentControl.Это нормально для замкнутых фигур, таких как прямоугольник или круг, но это проблема для диагональной линии.Рассмотрим это изображение, на котором представлены 3 такие фигуры и их ограничивающие рамки, показанные белым цветом:

enter image description here

Даже если я нахожусь в самом нижнем левом углуограничивающий прямоугольник вокруг линии, он все еще показывает курсор, и щелчки мыши все еще достигают моего пользовательского элемента.

Я хочу изменить это так, чтобы мышь считалась только «над» линией, обнаруженной, еслиЯ на определенном расстоянии от него.Мол, эта область в красном (простите за грубый рисунок).

enter image description here

Мой вопрос: как мне подойти к этому?Можно ли переопределить какую-нибудь виртуальную функцию, связанную с «HitTest», на моем ShapeItem?

Я уже знаю математику, чтобы выяснить, нахожусь ли я в нужном месте.Мне просто интересно, какой подход лучше выбрать.Какие функции мне переопределить?Или какие события я обрабатываю и т. Д. Я потерялся в документации WPF по тестированию Hit.Это вопрос переопределения HitTestCore или что-то в этом роде?

Теперь о коде.Я размещаю элементы в пользовательском ItemsControl, называемом «ShapesControl».который использует пользовательский контейнер "ShapeItem" для размещения объектов моей модели представления:

<Canvas x:Name="Scene" HorizontalAlignment="Left" VerticalAlignment="Top">

    <gcs:ShapesControl x:Name="ShapesControl" Canvas.Left="0" Canvas.Top="0"
                       ItemsSource="{Binding Shapes}">

        <gcs:ShapesControl.ItemsPanel>
            <ItemsPanelTemplate>
                <Canvas Background="Transparent" IsItemsHost="True" />
            </ItemsPanelTemplate>
        </gcs:ShapesControl.ItemsPanel>
        <gcs:ShapesControl.ItemTemplate>
            <DataTemplate DataType="{x:Type gcs:ShapeVm}">
                <Path ClipToBounds="False"
                      Data="{Binding RelativeGeometry}"
                      Fill="Transparent"/>
            </DataTemplate>
        </gcs:ShapesControl.ItemTemplate>

        <!-- Style the "ShapeItem" container that the ShapesControl wraps each ShapeVm ine -->

        <gcs:ShapesControl.ShapeItemStyle>
            <Style TargetType="{x:Type gcs:ShapeItem}"
                   d:DataContext="{d:DesignInstance {x:Type gcs:ShapeVm}}"
                   >
                <!-- Use a custom cursor -->

                <Setter Property="Background"  Value="Transparent"/>
                <Setter Property="Cursor"      Value="SizeAll"/>
                <Setter Property="Canvas.Left" Value="{Binding Path=Left, Mode=OneWay}"/>
                <Setter Property="Canvas.Top"  Value="{Binding Path=Top, Mode=OneWay}"/>


                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate  TargetType="{x:Type gcs:ShapeItem}">
                            <Grid SnapsToDevicePixels="True" Background="{TemplateBinding Panel.Background}">

                                <!-- First draw the item (i.e. the ShapeVm) -->

                                <ContentPresenter x:Name="PART_Shape"
                                                  Content="{TemplateBinding ContentControl.Content}"
                                                  ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}"
                                                  ContentTemplateSelector="{TemplateBinding ContentControl.ContentTemplateSelector}"
                                                  ContentStringFormat="{TemplateBinding ContentControl.ContentStringFormat}"
                                                  HorizontalAlignment="{TemplateBinding Control.HorizontalContentAlignment}"
                                                  VerticalAlignment="{TemplateBinding Control.VerticalContentAlignment}"
                                                  IsHitTestVisible="False"
                                                  SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}"
                                                  RenderTransformOrigin="{TemplateBinding ContentControl.RenderTransformOrigin}"/>

                            </Grid>

                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>

        </gcs:ShapesControl.ShapeItemStyle>
    </gcs:ShapesControl>
</Canvas>

Мой "ShapesControl"

public class ShapesControl : ItemsControl
{
    protected override bool IsItemItsOwnContainerOverride(object item)
    {
        return (item is ShapeItem);
    }

    protected override DependencyObject GetContainerForItemOverride()
    {
        // Each item we display is wrapped in our own container: ShapeItem
        // This override is how we enable that.
        // Make sure that the new item gets any ItemTemplate or
        // ItemTemplateSelector that might have been set on this ShapesControl.

        return new ShapeItem
        {
            ContentTemplate = this.ItemTemplate,
            ContentTemplateSelector = this.ItemTemplateSelector,
        };
    }
}

И мой "ShapeItem"

/// <summary>
/// A ShapeItem is a ContentControl wrapper used by the ShapesControl to
/// manage the underlying ShapeVm.  It is like the the item types used by
/// other ItemControls, including ListBox, ItemsControls, etc.
/// </summary>
[TemplatePart(Name="PART_Shape", Type=typeof(ContentPresenter))]
public class ShapeItem : ContentControl
{
    private ShapeVm Shape => DataContext as ShapeVm;
    static ShapeItem()
    {
        DefaultStyleKeyProperty.OverrideMetadata
            (typeof(ShapeItem), 
             new FrameworkPropertyMetadata(typeof(ShapeItem)));
    }

    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        // Toggle selection when the left mouse button is hit

        base.OnMouseLeftButtonDown(e);
        ShapeVm.IsSelected = !ShapeVm.IsSelected;
        e.Handled = true;

    }

    internal ShapesControl ParentSelector =>
        ItemsControl.ItemsControlFromItemContainer(this) as ShapesControl;
}

ShapeVm - это просто абстрактный базовый класс для моих моделей представлений.Примерно это:

public abstract class ShapeVm : BaseVm, IShape
{
    public virtual Geometry RelativeGeometry { get; }
    public bool   IsSelected { get; set; }
    public double Top        { get; set; }
    public double Left       { get; set; }
    public double Width      { get; }
    public double Height     { get; }      
 }

1 Ответ

0 голосов
/ 12 сентября 2018

Вы можете использовать класс ShapeItem, как показано ниже. Это Canvas с двумя дочерними объектами Path, один для тестирования попаданий, другой для отображения. Он напоминает несколько типичных свойств Shape (которые вы можете расширять в соответствии со своими потребностями).

public class ShapeItem : Canvas
{
    public ShapeItem()
    {
        var path = new Path
        {
            Stroke = Brushes.Transparent,
            Fill = Brushes.Transparent
        };
        path.SetBinding(Path.DataProperty,
            new Binding(nameof(Data)) { Source = this });
        path.SetBinding(Shape.StrokeThicknessProperty,
            new Binding(nameof(HitTestStrokeThickness)) { Source = this });
        Children.Add(path);

        path = new Path();
        path.SetBinding(Path.DataProperty,
            new Binding(nameof(Data)) { Source = this });
        path.SetBinding(Shape.FillProperty,
            new Binding(nameof(Fill)) { Source = this });
        path.SetBinding(Shape.StrokeProperty,
            new Binding(nameof(Stroke)) { Source = this });
        path.SetBinding(Shape.StrokeThicknessProperty,
            new Binding(nameof(StrokeThickness)) { Source = this });
        Children.Add(path);
    }

    public static readonly DependencyProperty DataProperty =
        Path.DataProperty.AddOwner(typeof(ShapeItem));

    public static readonly DependencyProperty FillProperty =
        Shape.FillProperty.AddOwner(typeof(ShapeItem));

    public static readonly DependencyProperty StrokeProperty =
        Shape.StrokeProperty.AddOwner(typeof(ShapeItem));

    public static readonly DependencyProperty StrokeThicknessProperty =
        Shape.StrokeThicknessProperty.AddOwner(typeof(ShapeItem));

    public static readonly DependencyProperty HitTestStrokeThicknessProperty =
        DependencyProperty.Register(nameof(HitTestStrokeThickness), typeof(double), typeof(ShapeItem));

    public Geometry Data
    {
        get => (Geometry)GetValue(DataProperty);
        set => SetValue(DataProperty, value);
    }

    public Brush Fill
    {
        get => (Brush)GetValue(FillProperty);
        set => SetValue(FillProperty, value);
    }

    public Brush Stroke
    {
        get => (Brush)GetValue(StrokeProperty);
        set => SetValue(StrokeProperty, value);
    }

    public double StrokeThickness
    {
        get => (double)GetValue(StrokeThicknessProperty);
        set => SetValue(StrokeThicknessProperty, value);
    }

    public double HitTestStrokeThickness
    {
        get => (double)GetValue(HitTestStrokeThicknessProperty);
        set => SetValue(HitTestStrokeThicknessProperty, value);
    }
}

public class ShapeItemsControl : ItemsControl
{
    protected override DependencyObject GetContainerForItemOverride()
    {
        return new ShapeItem();
    }

    protected override bool IsItemItsOwnContainerOverride(object item)
    {
        return item is ShapeItem;
    }
}

Вы бы использовали XAML, как это:

<gcs:ShapeItemsControl ItemsSource="{Binding Shapes}">
    <gcs:ShapeItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas/>
        </ItemsPanelTemplate>
    </gcs:ShapeItemsControl.ItemsPanel>
    <gcs:ShapeItemsControl.ItemContainerStyle>
        <Style TargetType="gcs:ShapeItem">
            <Setter Property="Data" Value="{Binding RelativeGeometry}"/>
            <Setter Property="Fill" Value="AliceBlue"/>
            <Setter Property="Stroke" Value="Yellow"/>
            <Setter Property="StrokeThickness" Value="3"/>
            <Setter Property="HitTestStrokeThickness" Value="15"/>
            <Setter Property="Cursor" Value="Hand"/>
        </Style>
    </gcs:ShapeItemsControl.ItemContainerStyle>
</gcs:ShapeItemsControl>

Однако вам может вообще не понадобиться класс ShapeItem и производный ItemsControl, когда вы помещаете Canvas в ItemTemplate обычного ItemsControl:

<ItemsControl ItemsSource="{Binding Shapes}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Canvas Cursor="Hand">
                <Path Data="{Binding RelativeGeometry}" Fill="Transparent"
                      Stroke="Transparent" StrokeThickness="15"/>
                <Path Data="{Binding RelativeGeometry}" Fill="AliceBlue"
                      Stroke="Yellow" StrokeThickness="3"/>
            </Canvas>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

Если вам также необходимо поддерживать выбор, вы должны использовать ListBox вместо ItemsControl. Третий путь в ItemTemplate может визуализировать состояние выбора.

<ListBox ItemsSource="{Binding Shapes}">
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas/>
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <ListBox.Template>
        <ControlTemplate TargetType="ListBox">
            <ItemsPresenter/>
        </ControlTemplate>
    </ListBox.Template>
    <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem">
            <Setter Property="IsSelected" Value="{Binding IsSelected}"/>
        </Style>
    </ListBox.ItemContainerStyle>
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Canvas Cursor="Hand">
                <Path Data="{Binding RelativeGeometry}" Fill="Transparent"
                      Stroke="Transparent" StrokeThickness="15"/>
                <Path Data="{Binding RelativeGeometry}"
                      Stroke="Green" StrokeThickness="7"
                      StrokeStartLineCap="Square" StrokeEndLineCap="Square"
                      Visibility="{Binding IsSelected,
                          RelativeSource={RelativeSource AncestorType=ListBoxItem},
                          Converter={StaticResource BooleanToVisibilityConverter}}"/>
                <Path Data="{Binding RelativeGeometry}" Fill="AliceBlue"
                      Stroke="Yellow" StrokeThickness="3"/>
            </Canvas>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>
...