Earth Engine
Earth Engine

Reputation: 10476

WPF hit test on custom control does not work

The following custom control

public class DummyControl : FrameworkElement
{
    private Visual visual;
    protected override Visual GetVisualChild(int index)
    {
        return visual;
    }
    protected override int VisualChildrenCount { get; } = 1;
    protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
    {
        var pt = hitTestParameters.HitPoint;
        return new PointHitTestResult(visual, pt);
    }

    public DummyControl()
    {
        var dv = new DrawingVisual();
        using (var ctx = dv.RenderOpen())
        {
            var penTransparent = new Pen(Brushes.Transparent, 0);
            ctx.DrawRectangle(Brushes.Green, penTransparent, new Rect(0, 0, 1000, 1000));
            ctx.DrawLine(new Pen(Brushes.Red, 3), new Point(0, 500), new Point(1000, 500));
            ctx.DrawLine(new Pen(Brushes.Red, 3), new Point(500, 0), new Point(500, 1000));
        }

        var m = new Matrix();
        m.Scale(0.5, 0.5);
        RenderTransform = new MatrixTransform(m);

        //Does work; but only the left top quater enters hit test
        //var hv = new HostVisual();
        //var vt = new VisualTarget(hv);
        //vt.RootVisual = dv;
        //visual = hv;

        //Never enters hit test
        visual = dv;
    }

}

The xaml

<Window x:Class="MyNamespace.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:MyNamespace"
    mc:Ignorable="d">

    <Border Width="500" Height="500">
        <local:DummyControl />
    </Border>
</Window>

Display a green area with two red coordinate lines through the center. But its hit testing behavior is not understandable for me.

How could the above being explained and how can I could make it work as expected (enters hit test on full range)?

(Originally, I just wanted to handle mouse events on the custom control. Some existing solutions pointed me to overriding the HitTestCore method. So if you could provide any idea that can let me handle mouse events, I don't have to make HitTestCore method working.)

Update

Clemen's answer is good if I decided to use DrawingVisual. However, when I use HostVisual and VisualTarget it is Not working without overriding HitTestCore, and even I do this, still only the top left quater will receive mouse events.

The original question also includes explainations. Also, the use of HostVisual allows me to run the render (time consuming in my real case) in another thread.

(Let me hightlight the code using HostVisual above)

    //Does work; but only the left top quater enters hit test
    //var hv = new HostVisual();
    //var vt = new VisualTarget(hv);
    //vt.RootVisual = dv;
    //visual = hv;

Any idea?

UPDATE #2

Clemen's new answer is still not working for my purpose. Yes, all the visual area receives hit test. However, what I wanted is to have the full viewport to receive hit test. Which, in his case, is the blank area as he scaled the full visual to the visual area.

Upvotes: 1

Views: 1447

Answers (2)

Clemens
Clemens

Reputation: 128145

In order to establish a visual tree (and thus make hit testing work by default), you also have to call AddVisualChild. From MSDN:

The AddVisualChild method sets up the parent-child relationship between two visual objects. This method must be used when you need greater low-level control over the underlying storage implementation of visual child objects. VisualCollection can be used as a default implementation for storing child objects.

Besides that, your control should re-render whenever its size changes:

public class DummyControl : FrameworkElement
{
    private readonly DrawingVisual visual = new DrawingVisual();

    public DummyControl()
    {
        AddVisualChild(visual);
    }

    protected override int VisualChildrenCount
    {
        get { return 1; }
    }

    protected override Visual GetVisualChild(int index)
    {
        return visual;
    }

    protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
    {
        using (var dc = visual.RenderOpen())
        {
            var width = sizeInfo.NewSize.Width;
            var height = sizeInfo.NewSize.Height;
            var linePen = new Pen(Brushes.Red, 3);

            dc.DrawRectangle(Brushes.Green, null, new Rect(0, 0, width, height));
            dc.DrawLine(linePen, new Point(0, height / 2), new Point(width, height / 2));
            dc.DrawLine(linePen, new Point(width / 2, 0), new Point(width / 2, height));
        }

        base.OnRenderSizeChanged(sizeInfo);
    }
}

When your control uses a HostVisual and a VisualTarget it would still have to re-render itself when its size changes, and also call AddVisualChild to establish a visual tree.

public class DummyControl : FrameworkElement
{
    private readonly DrawingVisual drawingVisual = new DrawingVisual();
    private readonly HostVisual hostVisual = new HostVisual();

    public DummyControl()
    {
        var visualTarget = new VisualTarget(hostVisual);
        visualTarget.RootVisual = drawingVisual;

        AddVisualChild(hostVisual);
    }

    protected override int VisualChildrenCount
    {
        get { return 1; }
    }

    protected override Visual GetVisualChild(int index)
    {
        return hostVisual;
    }

    protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParams)
    {
        return new PointHitTestResult(hostVisual, hitTestParams.HitPoint);
    }

    protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
    {
        using (var dc = drawingVisual.RenderOpen())
        {
            var width = sizeInfo.NewSize.Width;
            var height = sizeInfo.NewSize.Height;
            var linePen = new Pen(Brushes.Red, 3);

            dc.DrawRectangle(Brushes.Green, null, new Rect(0, 0, width, height));
            dc.DrawLine(linePen, new Point(0, height / 2), new Point(width, height / 2));
            dc.DrawLine(linePen, new Point(width / 2, 0), new Point(width / 2, height));
        }

        base.OnRenderSizeChanged(sizeInfo);
    }
}

You could now set a RenderTransform and still get correct hit testing:

<Border>
    <local:DummyControl MouseDown="DummyControl_MouseDown">
        <local:DummyControl.RenderTransform>
            <ScaleTransform ScaleX="0.5" ScaleY="0.5"/>
        </local:DummyControl.RenderTransform>
    </local:DummyControl>
</Border>

Upvotes: 4

AnjumSKhan
AnjumSKhan

Reputation: 9827

This will work for you.

public class DummyControl : FrameworkElement
    {                   
        protected override void OnRender(DrawingContext ctx)
        {          
            Pen penTransparent = new Pen(Brushes.Transparent, 0);
            ctx.DrawGeometry(Brushes.Green, null, rectGeo);
            ctx.DrawGeometry(Brushes.Red, new Pen(Brushes.Red, 3), line1Geo);
            ctx.DrawGeometry(Brushes.Red, new Pen(Brushes.Red, 3), line2Geo);

            base.OnRender(ctx);
        }

        RectangleGeometry rectGeo;
        LineGeometry line1Geo, line2Geo;

        public DummyControl()
        {
            rectGeo = new RectangleGeometry(new Rect(0, 0, 1000, 1000));
            line1Geo = new LineGeometry(new Point(0, 500), new Point(1000, 500));
            line2Geo = new LineGeometry(new Point(500, 0), new Point(500, 1000));

            this.MouseDown += DummyControl_MouseDown;
        }

        void DummyControl_MouseDown(object sender, MouseButtonEventArgs e)
        {

        }
    }

Upvotes: 0

Related Questions