自定义 WPF 控件中的鼠标事件

Mouse events in custom WPF Controls

我正在 WPF 中构建自定义控件,运行 在捕获输入鼠标事件时遇到了一些困难。我已经阅读了 routed events and class event handlers 上的各种文档,但它对我来说并不是很有效。我是 WPF 的新手,因为过去主要使用 Forms。

给定以下自定义控件可以包含多个children:

// Parent.cs
[ContentProperty(nameof(Children))]
public class Parent : Control
{
    private DrawingGroup _backingStore = new DrawingGroup();
    public List<UIElement> Children { get; } = new List<UIElement>();
    static Parent()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(Parent), new FrameworkPropertyMetadata(typeof(Parent)));
    }

    protected override void OnPreviewMouseMove(MouseEventArgs e)
    {
        // default event handler
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        /*do some custom drawing*/
        var backingContext = _backingStore.Open();
        // draw an X indicating the background
        backingContext.DrawRectangle(Background, new Pen(Brushes.White, 1), new Rect(0, 0, Width, Height));
        backingContext.DrawLine(pen, new Point(0, 0), new Point(Width - 1, Height - 1));
        backingContext.DrawLine(pen, new Point(0, Height - 1), new Point(Width - 1, 0));
        backingContext.Close();
        drawingContext.DrawDrawing(_backingStore);
    }

    protected override int VisualChildrenCount => Children.Count;

    protected override Visual GetVisualChild(int index) => Children[index];

    protected override Size ArrangeOverride(Size arrangeBounds)
    {
        foreach (FrameworkElement child in Children)
            child.Arrange(new Rect(0, 0, arrangeBounds.Width, arrangeBounds.Height));
        return new Size(arrangeBounds.Width, arrangeBounds.Height);
    }
}
// Child.cs
[ContentProperty(nameof(Children))]
public class Child : Control
{
    public List<UIElement> Children { get; } = new List<UIElement>();
    static Child()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(Parent), new FrameworkPropertyMetadata(typeof(Parent)));
    }

    protected override void OnPreviewMouseMove(MouseEventArgs e)
    {
        // NEVER FIRED
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        /*do some custom drawing*/
    }

    // same as Parent
}

// TestWindow.xaml

<Window x:Class="TestApp.TestWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TestApp"
        Title="TestWindow" Height="450" Width="800">
    <Grid>
        <local:Parent Background="White">
          <local:Child Background="Red" />
          <local:Child Background="Green" />
        </local:Parent>
    </Grid>
</Window>

// ParentStyle.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:TestApp">
    <Style TargetType="local:Parent">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:Parent">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <Style TargetType="local:Child">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:Child">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

我发现 Parent 收到鼠标移动事件。然而它的 children 不接收任何鼠标事件。它们不会向下传播,虽然我可以遍历 Children 并调用引入其他问题(命中测试等)的 RaiseEvent(e) 并且似乎是错误的答案。

您很接近,但您的想法太像 WinForms,而不太像 WPF。自定义渲染几乎从未在 WPF 中完成过——至少根据我的经验。该框架几乎可以处理您可能需要的所有内容,但我已经超前了。


面板基础知识

最重要的是:你不想继承Control,你想继承Panel。它的目的是“定位和排列child objects”。您将在 WPF 中找到的所有常用“容器”(GridStackPanel 等)都继承自此 class.

我认为您的大部分问题都源于 Control 本身不支持 child 元素这一事实。 Panel 的构建只是为了提供该功能,因此您会发现它已经实现了您必须声明的大部分属性,例如 Children.

Microsoft 有一个制作自定义面板的简单示例:
How to: Create a Custom Panel Element

您的 Parent class 应该看起来像这样:

public class Parent : Panel
{
    //We'll talk more about OnRender later
    protected override void OnRender(DrawingContext drawingContext)
    {
        var pen = new Pen(Brushes.Gray, 3);
        drawingContext.DrawLine(pen, new Point(0, 0), new Point(Width - 1, Height - 1));
        drawingContext.DrawLine(pen, new Point(0, Height - 1), new Point(Width - 1, 0));
    }

    protected override Size MeasureOverride(Size availableSize)
    {
        foreach (UIElement child in InternalChildren)
        {
            child.Measure(availableSize);
        }
        return availableSize;
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        foreach (UIElement child in InternalChildren)
        {
            child.Arrange(new Rect(finalSize));
        }
        return finalSize;
    }
}

这几乎可以完成您当前 Parent class 所做的一切。


布局

当然,上面的Panel只是把children层层叠叠而已,其实用处不大。为了解决这个问题,您需要了解 WPF 布局系统。关于这个主题有很多话要说,Microsoft 已经说了大部分 here。总结一下,主要有两种方法:

  • Measure,它询问一个元素它想要多大。

  • Arrange,它告诉控件它 实际有多大,以及它相对于 parent 的放置位置.

A Panel 的工作是从所有 children 中获取 Measure 结果,确定这些 children 的大小和位置,然后然后对那些 children 调用 Arrange 以分配最终的 Rect.


OnRender

请注意 Panel 不负责实际呈现其 children。 Panel 仅定位它们,渲染由 WPF 本身处理。

OnRender 方法可用于“向布局元素添加自定义图形效果”。 Microsoft 在此处给出了在自定义 Panel 中使用 OnRender 的示例:
How to: Override the Panel OnRender Method

在我之前展示的代码中,我保留了您最初的问题,并在 Panel 的背景上画了一个“X”。 Panel 的 children 会自动绘制在上面。

在检查了 Panel 的源代码和下面@Keith Stein 的回答提供的详细信息之后,我对 children 的 Parent 为 null 的猜测实际上是这个。为了 Children 正确接收事件,它们应该派生自 UIElementCollection 并使用正确的构造函数指示视觉和逻辑 children 的 parent(在这种情况下它们都是相同的) ).

不再需要 MeasureOverride/ArrangeOverride 等其他覆盖。

您可以通过继承 Panel 来节省自己的额外工作,或者使用如下的极简方法:

// Parent.cs
public class Parent : ControlBase
{
    private DrawingGroup _backingStore = new DrawingGroup();

    static Parent()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(Parent), new FrameworkPropertyMetadata(typeof(Parent)));
    }

    protected override void OnPreviewMouseMove(MouseEventArgs e)
    {
        // default event handler
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        /*do some custom drawing*/
        var backingContext = _backingStore.Open();
        // draw an X indicating the background
        backingContext.DrawRectangle(Background, new Pen(Brushes.White, 1), new Rect(0, 0, Width, Height));
        backingContext.DrawLine(pen, new Point(0, 0), new Point(Width - 1, Height - 1));
        backingContext.DrawLine(pen, new Point(0, Height - 1), new Point(Width - 1, 0));
        backingContext.Close();
        drawingContext.DrawDrawing(_backingStore);
    }
}

/// <summary>
/// A basic WPF control with children
/// </summary>
[ContentProperty(nameof(Children))]
public class ControlBase : Control
{
    private UIElementCollection _uiElementCollection;
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
    public UIElementCollection Children => InternalChildren;

    protected internal UIElementCollection InternalChildren
    {
        get
        {
            if (_uiElementCollection == null)
            {
                // First access on a regular panel
                EnsureEmptyChildren(this);
            }

            return _uiElementCollection;
        }
    }

    private void EnsureEmptyChildren(FrameworkElement logicalParent)
    {
        if (_uiElementCollection == null)
            _uiElementCollection = new UIElementCollection(this, logicalParent);
        else
            _uiElementCollection.Clear();
    }

    protected override int VisualChildrenCount => Children.Count;

    protected override Visual GetVisualChild(int index) => Children[index];
}