Search code examples
wpfgroupboxopacitymask

WPF GroupBox Header Customization


I have edited the standard GroupBox template as I wanted to customize it. Apart from other customizations, I wanted the GroupBox header to be Horizantally aligned in the Center instead of Left or Right. The alignment of the Header is not a problem however, the real problem is the OpacityMask defined for the Border controls. The opacity mask sets the transparent space behind the groupbox header where the borders are not drawn. I haven't able to figure it out how to place the transparent space / gap behind the groupbox header when I set the header to the center.

Here is how my XAML looks like: (Please navigate to the section beginning with "Border.OpacityMask" which sets the transparent gap in the border around the header)

<ControlTemplate x:Key="GroupBoxControlTemplate1" 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 Background="{TemplateBinding Background}" BorderBrush="Transparent" 
     BorderThickness="{TemplateBinding BorderThickness}" 
     CornerRadius="4" Grid.Column="1    " Grid.ColumnSpan="4" 
     Grid.Row="1" Grid.RowSpan="3" HorizontalAlignment="Stretch"/>

    <Border x:Name="Header" Grid.Column="2" Grid.RowSpan="2" HorizontalAlignment="Left" 
        Padding="3,1,3,0" VerticalAlignment="Stretch">
        <Border.Effect>
            <DropShadowEffect BlurRadius="10" Direction="334"/>
        </Border.Effect>
        <ContentPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" 
         Content="{TemplateBinding Header}" 
         ContentSource="Header" 
         ContentStringFormat="{TemplateBinding HeaderStringFormat}" 
         ContentTemplate="{TemplateBinding HeaderTemplate}" 
         RecognizesAccessKey="True" Height="Auto" 
         VerticalAlignment="Center"
         HorizontalAlignment="Center"
         OpacityMask="#FF3844BD" Margin="0,1,0,0">
        </ContentPresenter>
    </Border>

    <ContentPresenter Margin="{TemplateBinding Padding}" 
        SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" 
        Content="{TemplateBinding Content}" 
        ContentStringFormat="{TemplateBinding ContentStringFormat}" 
        ContentTemplate="{TemplateBinding ContentTemplate}" 
        Grid.Column="1" Grid.ColumnSpan="2" Grid.Row="2"/>
    <Border BorderBrush="White" BorderThickness="{TemplateBinding BorderThickness}" 
        CornerRadius="4" Grid.ColumnSpan="3" Grid.Row="1" Grid.RowSpan="3" RenderTransformOrigin="0.5,0.5" Margin="0">
        <Border.OpacityMask>
            <MultiBinding ConverterParameter="7" UpdateSourceTrigger="Default">
                <MultiBinding.Converter>
                    <BorderGapMaskConverter/>
                </MultiBinding.Converter>
                <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="4">
            <Border BorderBrush="White" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="4"/>
        </Border>
    </Border>
</Grid>

Many thanks for your help in advance.

-Wajahat


Solution

  • I had to do something similar some time ago, I wanted to create a GroupBox with two headers (one on the left and one on the right). I just used Reflector to get the code for BorderGapMaskConverter, and modified it to create my own converter. You could probably do the same here.


    EDIT: I modified my converter to make it work for a centered header.

    Here's the ControlTemplate

    <ControlTemplate TargetType="{x:Type GroupBox}">
        <Grid SnapsToDevicePixels="true">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="6"/>
                <ColumnDefinition Width="*"/>
                <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 CornerRadius="4"
                Grid.Row="1"
                Grid.RowSpan="3"
                Grid.Column="0"
                Grid.ColumnSpan="5"
                BorderThickness="{TemplateBinding BorderThickness}"
                BorderBrush="Transparent"
                Background="{TemplateBinding Background}"/>
            <Border x:Name="Header"
                Padding="3,1,3,0"
                Grid.Row="0"
                Grid.RowSpan="2"
                Grid.Column="2">
                <ContentPresenter ContentSource="Header" 
                              RecognizesAccessKey="True" 
                              SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
            </Border>
            <ContentPresenter Grid.Row="2"
                          Grid.Column="1"
                          Grid.ColumnSpan="3"
                          Margin="{TemplateBinding Padding}"
                          SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
            <Border CornerRadius="4"
                Grid.Row="1"
                Grid.RowSpan="3"
                Grid.ColumnSpan="5"
                BorderThickness="{TemplateBinding BorderThickness}"
                BorderBrush="White">
                <Border.OpacityMask>
                    <MultiBinding Converter="{StaticResource CenterBorderGapMaskConverter}">
                        <Binding ElementName="Header"
                             Path="ActualWidth"/>
                        <Binding RelativeSource="{RelativeSource Self}"
                             Path="ActualWidth"/>
                        <Binding RelativeSource="{RelativeSource Self}"
                             Path="ActualHeight"/>
                    </MultiBinding>
                </Border.OpacityMask>
    
                <Border BorderThickness="{TemplateBinding BorderThickness}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    CornerRadius="3">
                    <Border BorderThickness="{TemplateBinding BorderThickness}"
                        BorderBrush="White"
                        CornerRadius="2"/>
                </Border>
            </Border>
        </Grid>
    </ControlTemplate>
    

    And here's the converter:

    class CenterBorderGapMaskConverter : IMultiValueConverter
    {
        // Methods
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            Type type = typeof(double);
            if (values == null
                || values.Length != 3
                || values[0] == null
                || values[1] == null
                || values[2] == null
                || !type.IsAssignableFrom(values[0].GetType())
                || !type.IsAssignableFrom(values[1].GetType())
                || !type.IsAssignableFrom(values[2].GetType()))
            {
                return DependencyProperty.UnsetValue;
            }
    
            double pixels = (double)values[0];
            double width = (double)values[1];
            double height = (double)values[2];
            if ((width == 0.0) || (height == 0.0))
            {
                return null;
            }
            Grid visual = new Grid();
            visual.Width = width;
            visual.Height = height;
            ColumnDefinition colDefinition1 = new ColumnDefinition();
            ColumnDefinition colDefinition2 = new ColumnDefinition();
            ColumnDefinition colDefinition3 = new ColumnDefinition();
            colDefinition1.Width = new GridLength(1.0, GridUnitType.Star);
            colDefinition2.Width = new GridLength(pixels);
            colDefinition3.Width = new GridLength(1.0, GridUnitType.Star);
            visual.ColumnDefinitions.Add(colDefinition1);
            visual.ColumnDefinitions.Add(colDefinition2);
            visual.ColumnDefinitions.Add(colDefinition3);
            RowDefinition rowDefinition1 = new RowDefinition();
            RowDefinition rowDefinition2 = new RowDefinition();
            rowDefinition1.Height = new GridLength(height / 2.0);
            rowDefinition2.Height = new GridLength(1.0, GridUnitType.Star);
            visual.RowDefinitions.Add(rowDefinition1);
            visual.RowDefinitions.Add(rowDefinition2);
            Rectangle rectangle1 = new Rectangle();
            Rectangle rectangle2 = new Rectangle();
            Rectangle rectangle3 = new Rectangle();
            rectangle1.Fill = Brushes.Black;
            rectangle2.Fill = Brushes.Black;
            rectangle3.Fill = Brushes.Black;
            Grid.SetRowSpan(rectangle1, 2);
            Grid.SetRow(rectangle1, 0);
            Grid.SetColumn(rectangle1, 0);
            Grid.SetRow(rectangle2, 1);
            Grid.SetColumn(rectangle2, 1);
            Grid.SetRowSpan(rectangle3, 2);
            Grid.SetRow(rectangle3, 0);
            Grid.SetColumn(rectangle3, 2);
            visual.Children.Add(rectangle1);
            visual.Children.Add(rectangle2);
            visual.Children.Add(rectangle3);
            return new VisualBrush(visual);
        }
    
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            return new object[] { Binding.DoNothing };
        }
    }