none
HitTest Help With Custom Cursor Geometry

    Question

  • Hello all.

    I'm having some trouble getting HitTest to work using a custom UserControl as the source.  I am using MVVM (at least, mostly) to organize my code.

    A simplified version of my MainWindow.xaml looks like this:

    <Window ...>
        <Window.Resources>
            ...
        </Window.Resources>
    
        <Grid x:Name="layoutGrid">
            
            <ContentControl Margin="10" Content="{Binding ActiveModule}" />
            
            <Canvas Name="CursorCanvas">
                <ctrl:RightHandCursor x:Name="RightHandCursor" Canvas.Top="{Binding RightHandY}" Canvas.Left="{Binding RightHandX}" />
            </Canvas>
    
        </Grid>
    </Window>


    The "ActiveModule" ContentControl is dynamically replaced with different modules that the user would be interacting with (for example, a Slide Show or a Map).  Below that you can see my customer UserControl that represents the interface's cursor.

    I would like the "RightHandCursor" control to handle the HitTest, which would reveal what it is hovering over via a DependencyProperty.  The code currently looks like this:

        public partial class RightHandCursor : UserControl
        {
            private DispatcherTimer hitTestTimer = new DispatcherTimer();
    
            public static readonly DependencyProperty HoverElementProperty =
                DependencyProperty.Register(
                "HoverElement",
                typeof(UIElement),
                typeof(RightHandCursor),
                new PropertyMetadata(null));
    
            public RightHandCursor()
            {
                InitializeComponent();
    
                hitTestTimer.Tick += OnHitTestTimerTick;
                hitTestTimer.Interval = new TimeSpan(0, 0, 1);
                hitTestTimer.Start();
            }
    
            #region Properties
    
            public UIElement HoverElement
            {
                get { return (UIElement)GetValue(HoverElementProperty); }
                private set { SetValue(HoverElementProperty, value); }
            }
    
            #endregion Properties
    
            private void OnHitTestTimerTick(object sender, EventArgs e)
            {
                Point relativePoint = this.TransformToAncestor(Application.Current.MainWindow).Transform(new Point(0, 0));
                Point relativePoint2 = new Point(relativePoint.X + CursorBox.Width, relativePoint.Y + CursorBox.Height);
    
                RectangleGeometry cursorGeometry = new RectangleGeometry(new Rect(relativePoint, relativePoint2));
    
                VisualTreeHelper.HitTest(this, null,
                    new HitTestResultCallback(MyHitTestResultCallback),
                    new GeometryHitTestParameters(cursorGeometry));
    
            }
    
            public HitTestResultBehavior MyHitTestResultCallback(HitTestResult result)
            {
                // Retrieve the results of the hit test.
                IntersectionDetail intersectionDetail = ((GeometryHitTestResult)result).IntersectionDetail;
    
                switch (intersectionDetail)
                {
                    case IntersectionDetail.FullyContains:
                        // Set the behavior to return visuals at all z-order levels.
                        return HitTestResultBehavior.Continue;
    
                    case IntersectionDetail.Intersects:
                        // Set the behavior to return visuals at all z-order levels.
                        return HitTestResultBehavior.Continue;
    
                    case IntersectionDetail.FullyInside:
                        // Set the behavior to return visuals at all z-order levels.
                        return HitTestResultBehavior.Continue;
    
                    default:
                        return HitTestResultBehavior.Stop;
                }
            }
        }

    * NOTE: The callback function has been cleaned out for this post.  It does perform actions based on IntersectionDetails in the application.

    My trouble is knowing what, and how, to set the Visual to in the HitTest call.  In the code above, nothing ever fires -- the Visual is set the UserControl itself.  If I update the code to the following:

                VisualTreeHelper.HitTest(Application.Current.MainWindow, null,
                    new HitTestResultCallback(MyHitTestResultCallback),
                    new GeometryHitTestParameters(cursorGeometry));
    

    I do get results, but not ones I'm expecting.  In the test I'm running now I have set the "ActiveModule" to display a screen with a Button in it.  When I intersect with that Button I do get a result, but it is "Microsoft.Windows.Themes.ButtonChrome" instead of just simple a Button!  I also get all the containers holding the content.

    What I would like is to get a "Button" in return.  I'll be updating the Button to a custom control later, but for now I am just trying to get the regular stuff to work.

    Is there something different I should be setting my Visual to for the HitTest?  If the MainWindow is the best answer, why am I getting the ButtonChrome instead of the Button?

    Thank you for any help!

    Wednesday, July 18, 2012 11:45 PM

Answers

  • Thanks for the reply Sheldon.  I was ultimately able to solve the problem, after finding a few people with similar issues around other forums.  The solution was to add the following to the my control:

            protected override GeometryHitTestResult HitTestCore(GeometryHitTestParameters hitTestParameters)
            {
                RectangleGeometry geometry = new RectangleGeometry(VisualTreeHelper.GetDescendantBounds(this));
    
                return new GeometryHitTestResult
                 (this, geometry.FillContainsWithDetail(hitTestParameters.HitGeometry));
            }

    Even with a transparent background, the UserControl didn't want to acknowledge a visual form -- only the parts within.  Telling it check the physical bounds did the trick.

    Friday, July 20, 2012 4:02 PM

All replies

  • Hi Evil Closet Monkey

    Cool name!

    The reason you get ButtonChrome is because the default Template for a Button has the ButtonChrome as RootVisual.

    Extracted with Expression Blend's "Copy Style" on a default Button.

            <Style x:Key="ButtonStyle1" TargetType="{x:Type Button}">
                <Setter Property="FocusVisualStyle" Value="{StaticResource ButtonFocusVisual}"/>
                <Setter Property="Background" Value="{StaticResource ButtonNormalBackground}"/>
                <Setter Property="BorderBrush" Value="{StaticResource ButtonNormalBorder}"/>
                <Setter Property="BorderThickness" Value="1"/>
                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
                <Setter Property="HorizontalContentAlignment" Value="Center"/>
                <Setter Property="VerticalContentAlignment" Value="Center"/>
                <Setter Property="Padding" Value="1"/>
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type Button}">
                            <Microsoft_Windows_Themes:ButtonChrome x:Name="Chrome" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" RenderMouseOver="{TemplateBinding IsMouseOver}" RenderPressed="{TemplateBinding IsPressed}" RenderDefaulted="{TemplateBinding IsDefaulted}" SnapsToDevicePixels="true">
                                <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                            </Microsoft_Windows_Themes:ButtonChrome>
                            <ControlTemplate.Triggers>
                                <Trigger Property="IsKeyboardFocused" Value="true">
                                    <Setter Property="RenderDefaulted" TargetName="Chrome" Value="true"/>
                                </Trigger>
                                <Trigger Property="ToggleButton.IsChecked" Value="true">
                                    <Setter Property="RenderPressed" TargetName="Chrome" Value="true"/>
                                </Trigger>
                                <Trigger Property="IsEnabled" Value="false">
                                    <Setter Property="Foreground" Value="#ADADAD"/>
                                </Trigger>
                            </ControlTemplate.Triggers>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>

    You can simply override the default style with your own:

    <Style x:Key="ButtonStyle2" TargetType="{x:Type Button}"> <Setter Property="FocusVisualStyle" Value="{StaticResource ButtonFocusVisual}"/> <Setter Property="Background" Value="{StaticResource ButtonNormalBackground}"/> <Setter Property="BorderBrush" Value="{StaticResource ButtonNormalBorder}"/> <Setter Property="BorderThickness" Value="1"/> <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/> <Setter Property="HorizontalContentAlignment" Value="Center"/> <Setter Property="VerticalContentAlignment" Value="Center"/> <Setter Property="Padding" Value="1"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Button}"> <Button x:Name="Normal" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" SnapsToDevicePixels="true"> <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/> </Button> <ControlTemplate.Triggers> <Trigger Property="IsEnabled" Value="false"> <Setter Property="Foreground" Value="#ADADAD"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style>

    (remove the x:Key to make it implicit for all Buttons in Resource scope)

    This is exactly the same, but the ChromeButton is replaced with a Button and the ChromeButton's "Render" bits are removed.

    Alternatively, ButtonChrome is inherited from FrameworkElement, so it has a TemplatedParent property, which you may be able to cast back into the actual parent Button?

    Regards,
    Pete

    #PEJL




    Thursday, July 19, 2012 1:10 AM
  • Thanks for the reply Pete.

    I am having a heck of a time understanding how this HitTest is supposed to be working.  I've updated my HitTest Visual to point to the MainWindow, so I get everything.  I added a filter and am not understanding why I'm getting the results I am getting!

            public HitTestFilterBehavior MyHitTestFilterCallback(DependencyObject o)
            {
                if (o.GetType() == typeof(MyCustomeButton))
                {
                    Console.WriteLine("MyCustomControl found in Filter");
                    return HitTestFilterBehavior.Continue;
                }
    
                return HitTestFilterBehavior.ContinueSkipSelf;
            }
    
            public HitTestResultBehavior MyHitTestResultCallback(HitTestResult result)
            {
                // Retrieve the results of the hit test.
                IntersectionDetail intersectionDetail = ((GeometryHitTestResult)result).IntersectionDetail;
    
                switch (intersectionDetail)
                {
                    case IntersectionDetail.FullyContains:
                        // Set the behavior to return visuals at all z-order levels.
                        return HitTestResultBehavior.Continue;
    
                    case IntersectionDetail.Intersects:
                        Console.WriteLine("Result: " + result.VisualHit);
                        // Set the behavior to return visuals at all z-order levels.
                        return HitTestResultBehavior.Continue;
    
                    case IntersectionDetail.FullyInside:
                        // Set the behavior to return visuals at all z-order levels.
                        return HitTestResultBehavior.Continue;
    
                    default:
                        return HitTestResultBehavior.Stop;
                }
            }
    

    The "MyCustomButton" is a UserControl with a TextBlock in it right now (I wanted to see what happened w/o a Button).

    If I update the Filter to always return Continue then I ultimately get a return from the Result function, pointing out the TextBlock.  However, if in the code above the Filter sees the MyCustomerButton but the Result next sees the TextBlock!

    I'm really confused here.  I just want to know if my custom cursor (where this code is executing from) is hitting a control of a certain type.  Seems I'm missing something very simple in the process here.

    Thanks again!

    Thursday, July 19, 2012 1:45 AM
  • Hi Evil Closet Monkey,

    I check your code, I think it is correct, and the behavior is make sense, this is Hit Test Filter Callback default behavior.

    HitTestFilterCallback could filter out any visual we donot want to include in final hit test result, additional, your visual must hit testable, and then you could get this visual, you could check your control if is hit testable.

    for more information about HitTestFilterCallback, you could refer to this document:

    http://msdn.microsoft.com/en-us/library/ms752097.aspx

      Using a Hit Test Filter Callback

    You can use an optional hit test filter to restrict the objects that are passed on to the hit test results. This allows you to ignore parts of the visual tree that you are not interested in processing in your hit test results. To implement a hit test filter, you define a hit test filter callback function and pass it as a parameter value when you call the HitTest method.

    Best regards,


    Sheldon _Xiao[MSFT]
    MSDN Community Support | Feedback to us
    Microsoft
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    Friday, July 20, 2012 9:09 AM
  • Thanks for the reply Sheldon.  I was ultimately able to solve the problem, after finding a few people with similar issues around other forums.  The solution was to add the following to the my control:

            protected override GeometryHitTestResult HitTestCore(GeometryHitTestParameters hitTestParameters)
            {
                RectangleGeometry geometry = new RectangleGeometry(VisualTreeHelper.GetDescendantBounds(this));
    
                return new GeometryHitTestResult
                 (this, geometry.FillContainsWithDetail(hitTestParameters.HitGeometry));
            }

    Even with a transparent background, the UserControl didn't want to acknowledge a visual form -- only the parts within.  Telling it check the physical bounds did the trick.

    Friday, July 20, 2012 4:02 PM