【问题标题】:Hit-Testing in WPF for irregularly-shaped itemsWPF 中不规则形状项目的命中测试
【发布时间】:2018-09-12 01:31:52
【问题描述】:

我有一个包含在 ContentControl 派生类(“ShapeItem”)中的不规则形状的项目(线条形状)。我使用自定义光标设置样式,并在 ShapeItem 类中处理鼠标点击。

不幸的是,如果鼠标位于 ContentControl 矩形边界框内的任何位置,WPF 就会认为鼠标“悬停”在我的项目上。这对于像矩形或圆形这样的封闭形状是可以的,但对于对角线来说这是一个问题。考虑这个图像,其中显示了 3 个这样的形状,并且它们的边界框以白色显示:

即使我在该线周围边界框的最左下角,它仍然显示光标并且鼠标点击仍然到达我的自定义项。

我想更改此设置,以便仅当我在一定距离内时才认为鼠标“越过”检测到的线。就像,这个区域是红色的(请原谅粗略的绘图)。

我的问题是,我该如何处理?我是否在我的 ShapeItem 上覆盖了一些虚拟的“HitTest”相关函数?

我已经知道计算我是否在正确的地方。我只是想知道什么方法是最好的选择。我要覆盖哪些功能?或者我要处理什么事件,等等。我在关于命中测试的 WPF 文档中迷失了。是覆盖 HitTestCore 还是类似的问题?

现在是代码。我将项目托管在一个名为“ShapesControl”的自定义 ItemsControl 中。 它使用自定义的“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; }      
 }

【问题讨论】:

  • 你看过覆盖 HitTestCore 方法吗?
  • 真的那么简单吗?我只是覆盖 HitTestCore?
  • 你可能应该实现你的命中测试逻辑
  • 我刚试过,但不幸的是它仍然不能解决改变鼠标光标的问题。我还希望光标在用户移动它时反映它在不规则形状上。不幸的是,即使自定义覆盖了 HitTestCore,只要鼠标在内容控件的整个边界框上的任何位置,光标仍然会发生变化。
  • 正如 Mehrzad 指出的那样,覆盖 HitTestCore 是解决方案的一部分(感谢 Mehrzad)。但是关于显示正确鼠标光标的问题:经过相当多的搜索和摆弄,它开始看起来像如果我 a) 删除 ShapeItem 样式中的 Cursor 设置 b) 连接到 Mouse.QueryCursorEvent ,我可以在我的事件处理程序中进行手动命中测试并有效地覆盖那里的光标。无论如何,它似乎工作。如果有更清洁的方法,我会全力以赴......

标签: c# wpf hittest


【解决方案1】:

您可以使用如下所示的 ShapeItem 类。它是一个带有两个 Path 孩子的 Canvas,一个用于命中测试,一个用于显示。它类似于一些典型的 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>

但是,当您将 Canvas 放在常规 ItemsControl 的 ItemTemplate 中时,您可能根本不需要 ShapeItem 类和派生 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 中的第三个 Path 可以可视化选择状态。

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

【讨论】:

  • 欣赏答案。那肯定花了一些时间。这种方法是否也考虑到仅当光标位于所需的不规则形状上时才正确更改光标?
  • 关于选择,我最初的设计实际上让我的 ShapesControl 是一个自定义的 MultiSelector。但事实证明它是多余的,因为我的设计已经要求在(底层视图模型对象(“ShapeVm.IsSelected”)的属性中表示选择。所以我发现还试图让控件知道需要“选择什么”与视图模型协调。我没有从中得到任何东西。无论如何,没有人需要绑定到控件的“SelectedItems”属性。所以我回到了一个普通的 ItemsControl
  • 光标只有在形状上时才会改变。如果您不想将其放在内部,即仅在 Stroke 上,请不要设置 Fill。
猜你喜欢
  • 1970-01-01
  • 2018-01-20
  • 1970-01-01
  • 2012-07-07
  • 2011-01-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多