41

Have a look at this very simple example WPF program:

<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">

    <GroupBox>
        <GroupBox.Header>
            <CheckBox Content="Click Here"/>
        </GroupBox.Header>
    </GroupBox>
</Window>

So I have a GroupBox whose header is a CheckBox. We've all done something like this - typically you bind the content of the GroupBox in such a way that it's disabled when the CheckBox is unchecked.

However, when I run this application and click on the CheckBox, I've found that sometimes my mouse clicks are swallowed and the CheckBox's status doesn't change. If I'm right, it's when I click on the exact row of pixels that the GroupBox's top border sits on.

Can someone duplicate this? Why would this occur, and is there a way around it?

Edit: Setting the GroupBox's BorderThickness to 0 solves the problem, but obviously it removes the border, so it doesn't look like a GroupBox anymore.

Matt Hamilton
  • 200,371
  • 61
  • 386
  • 320

4 Answers4

24

Ian Oakes answer stuffs up the tab order such that the header comes after the content. It's possible to modify the control template such that the border can't receive focus.

To do this, modify the template so that the 2nd and 3rd borders (both in Grid Row 1) have IsHitTestVisible=false

Complete template below

<BorderGapMaskConverter x:Key="GroupBoxBorderGapMaskConverter" />

<Style x:Key="{x:Type GroupBox}" TargetType="{x:Type GroupBox}">
    <Setter Property="Control.BorderBrush" Value="#FFD5DFE5" />
    <Setter Property="Control.BorderThickness" Value="1" />
    <Setter Property="Control.Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type GroupBox}">
                <Grid SnapsToDevicePixels="True">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="6" />
                        <ColumnDefinition Width="Auto" />
                        <ColumnDefinition Width="*" />
                        <ColumnDefinition Width="6" />
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="*" />
                        <RowDefinition Height="6" />
                    </Grid.RowDefinitions>
                    <Border Name="Header" Padding="3,1,3,0" Grid.Row="0" Grid.RowSpan="2" Grid.Column="1">
                        <ContentPresenter ContentSource="Header" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
                    </Border>
                    <Border CornerRadius="4" Grid.Row="1" Grid.RowSpan="3" Grid.Column="0" Grid.ColumnSpan="4" BorderThickness="{TemplateBinding Control.BorderThickness}" BorderBrush="#00FFFFFF" Background="{TemplateBinding Control.Background}" IsHitTestVisible="False" />
                    <ContentPresenter Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Margin="{TemplateBinding Control.Padding}" SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}"/>
                    <Border CornerRadius="4" Grid.Row="1" Grid.RowSpan="3" Grid.ColumnSpan="4" BorderThickness="{TemplateBinding Control.BorderThickness}" BorderBrush="#FFFFFFFF" IsHitTestVisible="False">
                        <UIElement.OpacityMask>
                            <MultiBinding Converter="{StaticResource GroupBoxBorderGapMaskConverter}" ConverterParameter="7">
                                <Binding ElementName="Header" Path="ActualWidth" />
                                <Binding Path="ActualWidth" RelativeSource="{RelativeSource Self}" />
                                <Binding Path="ActualHeight" RelativeSource="{RelativeSource Self}" />
                            </MultiBinding>
                        </UIElement.OpacityMask>
                        <Border BorderThickness="{TemplateBinding Control.BorderThickness}" BorderBrush="{TemplateBinding Control.BorderBrush}" CornerRadius="3">
                            <Border BorderThickness="{TemplateBinding Control.BorderThickness}" BorderBrush="#FFFFFFFF" CornerRadius="2" />
                        </Border>
                    </Border>                        
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
Ray
  • 45,695
  • 27
  • 126
  • 169
19

It appears to be a subtle bug in the control template for the GroupBox. I found by editing the default template for the GroupBox and moving the Border named 'Header' to the last item in the control templates Grid element, the issue resolves itself.

The reason is that the one of the other Border elements with a TemplateBinding of BorderBrush was further down in the visual tree and was capturing the mouse click, that's why setting the BorderBrush to null allowed the CheckBox to correctly receive the mouse click.

Below is resulting style for the GroupBox. It is nearly identical to the default template for the control, except for the Border element named 'Header', which is now the last child of the Grid, rather than the second.

<BorderGapMaskConverter x:Key="BorderGapMaskConverter"/>

<Style x:Key="GroupBoxStyle1" TargetType="{x:Type GroupBox}">
    <Setter Property="BorderBrush" Value="#D5DFE5"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type GroupBox}">
                <Grid SnapsToDevicePixels="true">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="*"/>
                        <RowDefinition Height="6"/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="6"/>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="6"/>
                    </Grid.ColumnDefinitions>
                    <Border Grid.Column="0" Grid.ColumnSpan="4" Grid.Row="1" Grid.RowSpan="3" Background="{TemplateBinding Background}" BorderBrush="Transparent" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="4"/>
                    <ContentPresenter Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" Grid.Column="1" Grid.ColumnSpan="2" Grid.Row="2"/>
                    <Border Grid.ColumnSpan="4" Grid.Row="1" Grid.RowSpan="3" BorderBrush="White" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="4">
                        <Border.OpacityMask>
                            <MultiBinding Converter="{StaticResource BorderGapMaskConverter}" ConverterParameter="7">
                                <Binding Path="ActualWidth" ElementName="Header"/>
                                <Binding Path="ActualWidth" RelativeSource="{RelativeSource Self}"/>
                                <Binding Path="ActualHeight" RelativeSource="{RelativeSource Self}"/>
                            </MultiBinding>
                        </Border.OpacityMask>
                        <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="3">
                            <Border BorderBrush="White" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="2"/>
                        </Border>
                    </Border>
                    <Border x:Name="Header" Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" Padding="3,1,3,0">
                        <ContentPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" ContentSource="Header" RecognizesAccessKey="True"/>
                    </Border>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
Ian Oakes
  • 10,153
  • 6
  • 36
  • 47
  • Is the modified ControlTemplate very complex, Ian? Care to post it? – Matt Hamilton Sep 30 '08 at 10:55
  • Thanks Ian! I had to add a BorderGapConverter resource outside of this style for it to work, but once I did that it did the job nicely. – Matt Hamilton Oct 06 '08 at 23:04
  • Careful of the tab order with this. The content will now be before the header. See my answer for a slightly different answer (it still involves editing the control template). – Ray Oct 25 '10 at 12:35
11

An alternative solution I made is implementing OnApplyTemplate in a derived GroupBox:

public override void OnApplyTemplate()
{
  base.OnApplyTemplate();
  if (Children.Count == 0) return;

  var grid = GetVisualChild(0) as Grid;
  if (grid != null && grid.Children.Count > 3)
  {
    var bd = grid.Children[3] as Border;
    if (bd != null)
    {
      bd.IsHitTestVisible = false;
    }
  }
}
Rune Andersen
  • 1,650
  • 12
  • 15
  • The good thing about my solution is that you don't change the apperance of the groupbox so default theme won't be affected. Also it is very compact. – Rune Andersen Aug 15 '11 at 08:23
  • 1
    I'm using WPF 4.0, and it seems that GroupBox.Children does not exist. What should be the code for WPF 4? Should it be if (VisualChildrenCount == 0)? – Mas Sep 12 '11 at 07:44
  • +1, great solution, can't be simpler! @Mas: You could replaced it with `VisualChildrenCount == 0` but I think the whole check is unneeded (there is always one visual child). In fact, I think all the checks are not needed (plus the `base` call) so the whole method body could simply be one line: `((Grid)GetVisualChild(0)).Children[3].IsHitTestVisible = false;`. That's what I use and so far it works. – Allon Guralnek Sep 25 '12 at 20:20
  • WARNING: It seems that the root bug was fixed in .NET 4.5, and when installed on the machine this workaround causes all the controls inside the `GroupBox` to have `IsHitTestVisible = false` causing them not to be clickable! – Allon Guralnek Oct 03 '12 at 20:53
  • Ok. I'll check that as soon as I get 4.5 on my machine, thanks for warning about it! – Rune Andersen Oct 04 '12 at 08:11
4

If you change the GroupBox's BorderBrush, it works!

<GroupBox BorderBrush="{x:Null}">

I know this defeats the objective but it does prove where the problem lies!

rudigrobler
  • 17,045
  • 12
  • 60
  • 74
  • That might help anyway. We might be able to put a "real" Border behind the GroupBox somehow to fake it. I'll wait and see if we get any more responses but this might be as close as I get to an accepted answer. – Matt Hamilton Sep 30 '08 at 07:35
  • I have also asked about this on the WPF Disciples group: http://groups.google.com/group/wpf-disciples/browse_thread/thread/c21a1ecd6c843f75 – rudigrobler Sep 30 '08 at 07:43
  • 1
    Rudi - we just tried this to no avail. If you hit the pixel where the border *would* be the checkbox state does not change. Even setting the BorderBrush to Transparent doesn't help. – Matt Hamilton Sep 30 '08 at 23:04
  • 1
    Ah! Setting the BorderThickness to 0 fixes it. Of course, we lose the border of the GroupBox which means it doesn't look much like a GroupBox anymore. – Matt Hamilton Oct 01 '08 at 03:31
  • You should set the BorderBrush to null NOT transparent! – rudigrobler Oct 01 '08 at 06:26
  • Transparent is still a color and supports hit testing! – rudigrobler Oct 01 '08 at 06:30
  • 2
    Neither {x:Null} nor Transparent worked, but BorderThickness="0" did. Transparent can sometimes affect hit-testing, eg in a Window, so I thought it was worth a shot when null failed. – Matt Hamilton Oct 01 '08 at 08:41