简体   繁体   English

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

[英]Mouse events in custom WPF Controls

I'm building a custom control in WPF and running into some difficulties with capturing input mouse events.我正在 WPF 中构建一个自定义控件,并且在捕获输入鼠标事件时遇到了一些困难。 I've read the various documents on routed events and class event handlers however it's not really working for me.我已经阅读了有关路由事件class 事件处理程序的各种文档,但它并不适合我。 I am new to WPF as having mostly worked with Forms in the past.我是 WPF 的新手,因为过去主要与 Forms 合作。

Given the following custom control that can contain multiple 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 // 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 // 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>

I've found that Parent receives the raised mouse move events.我发现Parent接收到引发的鼠标移动事件。 However its children do not receive any mouse events.然而,它的孩子不会收到任何鼠标事件。 They aren't propagating downward, and while I could iterate through the Children and call RaiseEvent(e) that introduces other problems (hit testing, etc) and seems like the wrong answer.它们没有向下传播,虽然我可以遍历 Children 并调用 RaiseEvent(e),但它会引入其他问题(命中测试等)并且似乎是错误的答案。

You're close, but you're thinking too much like WinForms and not quite enough like WPF.你已经接近了,但你想的太多像 WinForms 而不是 WPF。 Custom rendering is hardly ever done in WPF- at least not in my experience.在 WPF 中几乎没有完成自定义渲染——至少在我的经验中没有。 The framework handles just about everything you could need, but I'm getting ahead of myself.该框架几乎可以处理您可能需要的所有内容,但我已经超越了自己。


Panel Basics面板基础知识

First thing's first: you don't want to inherit from Control , you want to inherit from Panel .首先是第一件事:你不想从Control继承,你想从Panel继承。 It's purpose is to "position and arrange child objects".它的目的是“定位和排列子对象”。 All the usual "containers" you will find in WPF ( Grid , StackPanel , etc.) inherit from this class.您将在 WPF( GridStackPanel等)中找到的所有常用“容器”都继承自此 class。

I think most of your problems are stemming from the fact that Control doesn't, by itself, support child elements.我认为您的大多数问题都源于Control本身不支持子元素的事实。 Panel is build to provide just that functionality, and so you will find it already implements most of the properties you had to declare, such as Children . Panel就是为了提供该功能而构建的,因此您会发现它已经实现了您必须声明的大部分属性,例如Children

Microsoft has a simple example for makeing a custom panel:微软有一个制作自定义面板的简单示例:
How to: Create a Custom Panel Element 如何:创建自定义面板元素

Your Parent class should end up looking something like this:您的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;
    }
}

This does just about everything your current Parent class did.这几乎可以完成您当前的Parent class 所做的所有事情。


Layout布局

Of course, the above Panel just stacks children on top of each other, so it's not really useful.当然,上面的Panel只是将孩子们堆叠在一起,所以它并不是很有用。 In order to fix that, you'll need to understand the WPF layout system.为了解决这个问题,您需要了解 WPF 布局系统。 There's plenty to say on the subject, and Microsoft has said most of it here .关于这个主题有很多话要说,微软在这里已经说了大部分。 To summarize a bit, there are two main methods:总结一下,主要有两种方法:

  • Measure , which asks an element how big it wants to be. Measure ,它询问一个元素它想要多大。

  • Arrange , which tells the control how big it will actually be and where it will be placed relative to its parent. Arrange ,它告诉控件它实际上有多大以及它相对于其父级的放置位置。

A Panel 's job is to take the Measure result from all of it's children, determine the size and position of those children, and then call Arrange on those children to assign a final Rect . Panel的工作是从它的所有子项中获取Measure结果,确定这些子项的大小和 position,然后对这些子项调用Arrange以分配最终的Rect


OnRender渲染

Note that the Panel is not responsable for actually rendering its children.请注意, Panel不负责实际渲染其子级。 The Panel only positions them, the rendering is handled by WPF itself. Panel只定位它们,渲染由 WPF 自己处理。

The OnRender method can be used to "add custom graphical effects to a layout element". OnRender方法可用于“将自定义图形效果添加到布局元素”。 Microsoft gives an exmaple of using OnRender in a custom Panel here: Microsoft 在此处提供了在自定义Panel中使用OnRender的示例:
How to: Override the Panel OnRender Method 如何:覆盖面板 OnRender 方法

In the code I showed previously, I kept with your original question and drew an "X" on the Panel 's background.在我之前展示的代码中,我保留了您原来的问题,并在Panel的背景上画了一个“X”。 The Panel 's children are then drawn on top of that automatically.然后Panel的孩子会自动绘制在上面。

After examining the source code of Panel and details provided by @Keith Stein's answer below, my guess of the children's Parent being null is in fact the cause of this.在检查了面板的源代码和下面@Keith Stein 的答案提供的详细信息之后,我猜测孩子的父母是 null 实际上是造成这种情况的原因。 In order for Children to receive events properly, they should be derived from UIElementCollection and use the correct constructor indicating the parent of the visual and logical children (they are both the same in this case).为了让 Children 能够正确接收事件,它们应该从 UIElementCollection 派生并使用正确的构造函数来指示可视子级和逻辑子级的父级(在这种情况下它们都是相同的)。

The other overrides such as MeasureOverride/ArrangeOverride were no longer needed.不再需要其他覆盖,例如 MeasureOverride/ArrangeOverride。

You could save yourself the additional work by inheriting from Panel, or use the minimalist approach as follows:您可以通过从 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];
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM