Search code examples
c#wpfgridchildrenmousecapture

How To Let A Grid Capture The Mouse, But Still Allow It's Children To Handle Click Events


Sorry in advance if the title is confusing. Here's the situation. I have a grid called grdFilters. This grid has a series of CheckBoxes within it (one per row). Normally this grid is hidden. But I wanted it to show up when prompted (on button click) and leave when the user clicks somewhere other than the grid. To handle outside control mouse clicks I tried first capturing the mouse as such:

    AddHandler(Mouse.PreviewMouseDownOutsideCapturedElementEvent, new MouseButtonEventHandler(HandleClickOutsideOfControl));

    private void HandleClickOutsideOfControl(object sender, MouseButtonEventArgs e)
    {
        if (this.filters) //Check if the Filters grid is visible
        {
            ShowHideMenu("sbHideFilters", grdFilters); //Method that hides the grid
            Mouse.Capture(null); //Release the mouse
        }
    }

    private void btnFilters_Click(object sender, RoutedEventArgs e)
    {
        if (!this.filters) //Check if the filters grid is shown
        {
            ShowHideMenu("sbShowFilters", grdFilters); //Method that reveals the grid
            Mouse.Capture(grdFilters); //Capture the mouse
        }
    }

The problem is that while the Filters grid has captured the mouse, none of the grids children (the Check Boxes) can be clicked on. I would really like to find a way to detect when the mouse is clicked outside of the grid while still allowing the children of the grid to accept mouse down events. Any help would be greatly appreciated, thanks in advance.

As per request here is some of my Xaml:

<Grid>
    <Label x:Name="label" Content="Events" HorizontalAlignment="Center" VerticalAlignment="Top"/>
    <ScrollViewer HorizontalAlignment="Left" Height="619" Margin="0,26,0,0" VerticalAlignment="Top" Width="450" VerticalScrollBarVisibility="Hidden">
        <Grid x:Name="Schedule" HorizontalAlignment="Left" Height="Auto" VerticalAlignment="Top" Width="450" Margin="10,0,0,0"/>
    </ScrollViewer>
    <Grid x:Name="grdFilters" HorizontalAlignment="Left" Height="619" Margin="490,26,-176,0" VerticalAlignment="Top" Width="135" Background="{StaticResource TransparentBackground}" Panel.ZIndex="95">
        <CheckBox x:Name="chckAll" Content="All" HorizontalAlignment="Left" Margin="0,10,0,0" VerticalAlignment="Top" Checked="chckAll_Checked" Unchecked="chckAll_Unchecked"/>
        <Grid x:Name="grdFilters" HorizontalAlignment="Left" Height="588" Margin="0,31,0,0" VerticalAlignment="Top" Width="136"/>
    </Grid>
    <Button x:Name="btnFilters" Content="" Margin="436,223,-18,0" VerticalAlignment="Top" Background="Cyan" Opacity="0.15" Style="{StaticResource MyTabStyle}" Height="80" Click="btnFilters_Click"/>

</Grid>

The only thing I left out were the Resource Dictionaries and the page definition itself.


Solution

  • I think the Mouse.Capture and PreviewMouseDownOutsideCapturedElementEvent and are too specific for what you want.

    I would rather use a hitResultsList, which can be used in a lot of different scenarios:

    I slightly modified eh AddHandler

    AddHandler(Mouse.PreviewMouseDownEvent, new MouseButtonEventHandler(HandleMouseDown));
    

    And I added the VisualTreeHelper.HitTest logic

        //List to store all the elements under the cursor
        private List<DependencyObject> hitResultsList = new List<DependencyObject>();
    
        private void HandleMouseDown(object sender, MouseButtonEventArgs e)
        {
    
            Point pt = e.GetPosition((UIElement)sender);
            hitResultsList.Clear();
    
            //Retrieving all the elements under the cursor
            VisualTreeHelper.HitTest(this, null,
                new HitTestResultCallback(MyHitTestResult),
                new PointHitTestParameters(pt));
    
            //Testing if the grdFilters is under the cursor
            if (!hitResultsList.Contains(this.grdFilters) && grdFilters.Visibility == System.Windows.Visibility.Visible)
            {
                grdFilters.Visibility = System.Windows.Visibility.Hidden;
            }
        }
    
        //Necessary callback function
        private HitTestResultBehavior MyHitTestResult(HitTestResult result)
        {
            hitResultsList.Add(result.VisualHit);
            return HitTestResultBehavior.Continue;
        }
    

    that way you can also remove the Mouse.Capture call from the btnFilters_Click:

        private void btnFilters_Click(object sender, RoutedEventArgs e)
        {
            if (grdFilters.Visibility != System.Windows.Visibility.Visible)
                grdFilters.Visibility = System.Windows.Visibility.Visible; }
        }