0

I'm building a custom control in WPF and running into some difficulties with capturing input mouse events. I've read the various documents on routed events and class event handlers however it's not really working for me. I am new to WPF as having mostly worked with Forms in the past.

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

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

I've found that Parent receives the raised mouse move events. 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.

Michael Brown
  • 1,585
  • 1
  • 22
  • 36
  • The only thing that catches my eye is `OnRender`. I've *never* needed to use `OnRender` in WPF, but maybe whatever your drawing is sitting on top of the children and blocking the mouse input? I couldn't say without seeign the code. – Keith Stein May 22 '20 at 02:39
  • @KeithStein I've updated the sample with OnRender. The control will be custom painted, as will its children. In the basic example I'm drawing the full size of the window, and its children may be sized differently. They do overlap but I've found none of the children receive mouse events. The intended end use will be a docked panel manager, so it needs to manage its children and how they are painted. Perhaps I'm going about this wrong, but I'm not sure why custom rendering would affect events unless the control is transparent. I noticed the child's Parent is null, not sure if its related. – Michael Brown May 22 '20 at 03:04
  • I'm starting to get some ideas, but I'll need to reproduce everything so I can test. Can you edit your question to include the default `Style` for `Parent` and `Child` (i.e. the `Style` where the `ControlTemplate` is defined)? – Keith Stein May 22 '20 at 03:47
  • Done, I just used a basic style since everything is drawn in OnRender. One thing I noticed is the Children of `Parent` have a RenderSize, ActualHeight/ActualWidth of (0,0). Maybe I'm doing something wrong when setting up the Children - they render just fine but events are non-existent. Maybe something to do with HitTesting? Found a [hint](https://stackoverflow.com/questions/3269120/wpf-frameworkelement-not-receiving-mouse-input) – Michael Brown May 22 '20 at 03:58
  • Interesting - I resolved that by calling Measure() on the children, however didn't change the events. I did notice HitTestCore() gets fired on the parent but not children, so something in there is probably related. – Michael Brown May 22 '20 at 04:26

2 Answers2

1

You're close, but you're thinking too much like WinForms and not quite enough like WPF. Custom rendering is hardly ever done in WPF- at least not in my experience. 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. 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.

I think most of your problems are stemming from the fact that Control doesn't, by itself, support child elements. 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.

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:

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.


Layout

Of course, the above Panel just stacks children on top of each other, so it's not really useful. In order to fix that, you'll need to understand the WPF layout system. 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.

  • Arrange, which tells the control how big it will actually be and where it will be placed relative to its parent.

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.


OnRender

Note that the Panel is not responsable for actually rendering its children. The Panel only positions them, the rendering is handled by WPF itself.

The OnRender method can be used to "add custom graphical effects to a layout element". Microsoft gives an exmaple of using OnRender in a custom Panel here:
How to: Override the Panel OnRender Method

In the code I showed previously, I kept with your original question and drew an "X" on the Panel's background. The Panel's children are then drawn on top of that automatically.

Community
  • 1
  • 1
Keith Stein
  • 6,235
  • 4
  • 17
  • 36
  • I'll try this out and review, thanks for the thorough explanation! – Michael Brown May 22 '20 at 05:19
  • I confirmed this works. I'm also reviewing the source code for Panel to see what I'm missing in my implementation that makes it work. When inheriting from Panel, the children have a proper Parent and I suspect this is related to why it wasn't working. I also noticed that the Loaded event is never fired in my case. If I figure out specifically what is missing to make this work I'll post a follow up for others. – Michael Brown May 25 '20 at 21:43
0

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

The other overrides such as MeasureOverride/ArrangeOverride were no longer needed.

You could save yourself the additional work by inheriting from Panel, or use the minimalist approach as follows:

// 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];
}
Michael Brown
  • 1,585
  • 1
  • 22
  • 36