自定义 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 中找到的所有常用“容器”(Grid
、StackPanel
等)都继承自此 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];
}
我正在 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 中找到的所有常用“容器”(Grid
、StackPanel
等)都继承自此 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];
}