WPF: Making a Expander look like a GroupBox

This may be long, but at the end of the post, you should be able to change the look of any WPF control.

I have a Win32/WFC application that I’m porting to .Net/WPF.  It is a simulator, so has lots of values to set/tweak.  The current application has lots of GroupBox clustering options together, yet most are only needed some of the time (like before starting), and some options should only be used if other options are not.

With this in mind, I wanted to be able to expand/collapse groups.

My first attempt was to add a button to the header of a GroupBox, and in the code behind alter the Visibility=”Collapsed”, but that felt clunky.

I then found the Expander control, which does exactly what I want, yet as a UI control I find it ugly.  I guess it is good for building Outlook tool panels.  This is where the power of WPF shines.  I can take the expander and give it a new skin and make it look like a group box, but keep the boarder visible while the content is collapsed.

GroupBox and Expander in default style
GroupBox and Expander in default style

The first place I looked was is MSDN, and tried the Expander ControlTemplate example, but the theme was completely different from the normal theme, and that was what I was wanting to capture.

So I then asked on the NZ .Net mailing list, and Ivan came up with some helpful options:

  • Use Expression Blend 2
  • Use the BAML Viewer Reflector add-in
  • or try MSDN under Controls > Control Customisation > ControlTemplate examples (the above example)

So I tried the idea’s in exciting order first, aka Reflector and the BAML Viewer. In short this was the wrong choice, here’s why, it took me ages to find that I needed to look at the correct DLL:

You will need to open one of the theme DLLs, e.g. PresentationFramework.Luna.dll or PresentationFramework.Aero.dll (use Open Cache since they are in the GAC) - Ivan

Once I had the correct Xaml, I realised that the internally referenced static resources were not named correctly, therefore you could not tell what was used where. That was a deal breaker.

So I then turned to what I thought would be the trickiest option installing and using Expression Blend 2.

First wrong assumption was, that it was now released and thus I’d need a license. It’s still in development, so is still free.  It was also small (compared to Visual Studio installers) at 25MB.

Once I got Blend installed, changing the look of controls is SO EASY:

  1. Add the control you want to copy the look of, to the design surface

  2. Right click on the control

  3. Click Edit Control Parts (Template)

  4. Click _Edit a Copy

    How to copy a WPF control’s controll template
    How to copy a WPF control’s controll template
  5. Save as something.xaml

  6. Add the control you want to change to the design surface

  7. Repeat steps 2 - 4

  8. Save as IntelligentName.xaml

  9. Merge the two templates to give you the desired look/feel/interaction

After having done this I ended up with a Expander that looked like a GroupBox

WPF Expander with the look of a GroupBox
WPF Expander with the look of a GroupBox

You just add

Style="{StaticResource GroupBoxExpander}"

to your Expander, and I also tend to set

IsExpanded="True"

The code for the GroupBoxExpander.Xaml is at end of this post. Remember to include the ResourceDictionary in your App.xaml via a Merged Resource Dictionaries

Some things I learnt on the way:

  • There are some quite clever uses of a Grid, to get the Board half way through the text of a GroupBox

  • I really like WPF’s separation of concerns

  • I quite like typing Xaml by hand

      <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    
      <Style x:Key="GroupBoxExpanderToggleButtonStyle" TargetType="{x:Type ToggleButton}">
      <Setter Property="Template">
      <Setter.Value>
      <ControlTemplate TargetType="{x:Type ToggleButton}">
      <Grid SnapsToDevicePixels="False" Background="Transparent">
      <Ellipse HorizontalAlignment="Center" x:Name="circle" VerticalAlignment="Center" Width="15" Height="15" Fill="{DynamicResource ButtonNormalBackgroundFill}" Stroke="DarkGray"/>
      <Ellipse Visibility="Hidden" HorizontalAlignment="Center" x:Name="shadow" VerticalAlignment="Center" Width="13" Height="13" Fill="{DynamicResource ExpanderShadowFill}"/>
      <Path SnapsToDevicePixels="false" x:Name="arrow" VerticalAlignment="Center" HorizontalAlignment="Center" Stroke="#666" StrokeThickness="2" Data="M1,1 L4,4 7,1" />
      </Grid>
    
      <ControlTemplate.Triggers>
      <Trigger Property="IsChecked" Value="true">
      <Setter Property="Data" TargetName="arrow" Value="M 1,4 L 4,1 L 7,4"/>
      </Trigger>
      <Trigger Property="IsMouseOver" Value="true">
      <Setter Property="Stroke" TargetName="circle" Value="#666"/>
      <Setter Property="Stroke" TargetName="arrow" Value="#222"/>
      <Setter Property="Visibility" TargetName="shadow" Value="Visible"/>
      </Trigger>
      </ControlTemplate.Triggers>
      </ControlTemplate>
      </Setter.Value>
      </Setter>
      </Style>
    
      <SolidColorBrush x:Key="GroupBoxBorderBrush" Color="#D0D0BF"/>
      <SolidColorBrush x:Key="GroupBoxHeaderBrush" Color="#0046D5"/>
      <BorderGapMaskConverter x:Key="BorderGapMaskConverter"/>
    
      <Style x:Key="GroupBoxExpander" TargetType="Expander">
      <Setter Property="BorderBrush" Value="{StaticResource GroupBoxBorderBrush}" />
      <Setter Property="BorderThickness" Value="1" />
      <Setter Property="Template">
      <Setter.Value>
      <ControlTemplate TargetType="{x:Type Expander}">
      <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 CornerRadius="4" Grid.Row="1" Grid.RowSpan="3" Grid.Column="0" Grid.ColumnSpan="4" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="Transparent" Background="{TemplateBinding Background}" />
      <Border x:Name="Header" Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" Padding="3,0,3,0">
      <Grid SnapsToDevicePixels="False" Background="Transparent" Grid.Column="1" Grid.Row="0" Grid.RowSpan="2">
      <Grid.ColumnDefinitions>
      <ColumnDefinition />
      <ColumnDefinition />
      </Grid.ColumnDefinitions>
      <ToggleButton Grid.Column="0" MinHeight="0" MinWidth="0"
      Name="HeaderToggle" Style="{StaticResource GroupBoxExpanderToggleButtonStyle}"
      IsChecked="{Binding Path=IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" >
      </ToggleButton>
      <ContentPresenter ContentSource="Header" RecognizesAccessKey="true"
      TextElement.Foreground="{StaticResource GroupBoxHeaderBrush}"
      SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="3,0,0,0" />
      </Grid>
      </Border>
    
      <ContentPresenter x:Name="ExpandSite" Visibility="Collapsed" Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
    
      <Border Grid.Row="1" Grid.RowSpan="3" Grid.ColumnSpan="4" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}" 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>
    
      </Grid>
      <ControlTemplate.Triggers>
      <Trigger Property="IsExpanded" Value="true">
      <Setter Property="Visibility" TargetName="ExpandSite" Value="Visible"/>
      </Trigger>
      <Trigger Property="IsEnabled" Value="false">
      <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
      </Trigger>
    
      </ControlTemplate.Triggers>
      </ControlTemplate>
      </Setter.Value>
      </Setter>
      </Style>
      </ResourceDictionary>
    

Comments:

Philip Patrick 2009-01-14 01:13:29

Thanks for good and useful post!

I have altered a bit your code, so the text in Expander’s header will go to toggle button, this way it will be clickable as well. Here is how I did it:

1. In the control template for ToggleButton, I have surrounded Grid with StackPanel with Orientation set to Horizontal. Inside it, I have addede ContentPresenter

2. I have added ToggleButton.Content tag into ToggleButton in the expander style itself and moved ContentPresenter inside it (it is currently follows immediately that ToggleButton.

That’s it :)


Simeon 2009-01-14 08:23:32

Hi Philip,

I also toyed with that idea, or making the button the whole header.

That’s the magic of WPF, once you learn the secret, you can do it your way.


Klaus 2009-05-30 07:57:17

Excellent stuff guys. The beauty of WPF indeed.


TimothyP 2009-12-23 19:41:46

Hi,
Great example.

I did have to move the:

<Border x:Name="Header" Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" Padding="3,0,3,0">
<Grid SnapsToDevicePixels="False" Background="Transparent" Grid.Column="1" Grid.Row="0" Grid.RowSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ToggleButton Grid.Column="0" MinHeight="0" MinWidth="0"
Name="HeaderToggle" Style="{StaticResource GroupBoxExpanderToggleButtonStyle}"
IsChecked="{Binding Path=IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" >
</ToggleButton>
<ContentPresenter ContentSource="Header" RecognizesAccessKey="true"
TextElement.Foreground="{StaticResource GroupBoxHeaderBrush}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="3,0,0,0" />
</Grid>
</Border>

to be the last item in the grid in order to make it more responsive.

(Otherwise the expander does not open/close if you happen to click at the same height of the border


Simeon 2009-12-24 08:14:41

Cheers Timothy, Not sure what you had to move as it appears to have gotten mangled, if it was code like stuff, email and I’ll paste it in.


AdamH 2010-05-14 06:48:04

This is exactly what I was looking for, thanks!!!

I did do some modifications though. Like TimothyP said, the tags needed rearranging to work properly. I also set it up so that the ToggleButton was an image; just to add a splash of color.


Simeon 2010-05-14 06:51:06

Hi Adam,

Glad to hear it helped, if you email me the code directly, I’ll post it in for you.

Simeon


AdamH 2010-05-14 15:32:27

No problem. I also implemented Philip Patrick’s suggestion about the header inside the ToggleButton.

<SolidColorBrush x:Key="GroupBoxBorderBrush" Color="Gray"/>
<SolidColorBrush x:Key="GroupBoxHeaderBrush" Color="#0046D5"/>
<BorderGapMaskConverter x:Key="BorderGapMaskConverter"/>


<Style x:Key="GroupBoxExpanderToggleButtonStyle" TargetType="{x:Type ToggleButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<StackPanel Orientation="Horizontal">
<Grid SnapsToDevicePixels="False" Background="Transparent">
<Ellipse Visibility="Hidden" HorizontalAlignment="Center" x:Name="shadow" VerticalAlignment="Center" Width="18" Height="18" Fill="LightBlue"/>
<Image HorizontalAlignment="Center" x:Name="arrow" VerticalAlignment="Center" Height="16" Width="16" Source="Resources/DownArrow.png"/>
</Grid>
<ContentPresenter Margin="0,0,-2,0" ></ContentPresenter>
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="true">
<Setter Property="Source" TargetName="arrow" Value="Resources/UpArrow.png"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Visibility" TargetName="shadow" Value="Visible"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

<Style x:Key="GroupBoxExpander" TargetType="Expander">
<Setter Property="BorderBrush" Value="{StaticResource GroupBoxBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Focusable" Value="False"/>
<Setter Property="IsExpanded" Value="True" />
<Setter Property="Margin" Value="5,5,5,0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Expander}">
<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 CornerRadius="4" Grid.Row="1" Grid.RowSpan="3" Grid.Column="0" Grid.ColumnSpan="4" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="Transparent" Background="{TemplateBinding Background}" />


<ContentPresenter x:Name="ExpandSite" Visibility="Collapsed" Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />

<Border Grid.Row="1" Grid.RowSpan="3" Grid.ColumnSpan="4" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}" 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>
<Border x:Name="Header" Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" Padding="3,0,3,0">
<Grid SnapsToDevicePixels="False" Background="Transparent" Grid.Column="1" Grid.Row="0" Grid.RowSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ToggleButton Grid.Column="0" MinHeight="0" MinWidth="0" Margin="-2,0,0,2"
Name="HeaderToggle" Style="{StaticResource GroupBoxExpanderToggleButtonStyle}"
IsChecked="{Binding Path=IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" >
<ToggleButton.Content>
<ContentPresenter ContentSource="Header" RecognizesAccessKey="true"
TextElement.Foreground="{StaticResource GroupBoxHeaderBrush}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left"/>
</ToggleButton.Content>
</ToggleButton>
</Grid>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="true">
<Setter Property="Visibility" TargetName="ExpandSite" Value="Visible"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>

</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

DaveCS 2011-03-25 18:59:49

Thanks a million! I didn’t have the need to make the expander look like a groupbox but I did really need to stylize my expanders.

It was really easy after seeing your example. Having Blend, I was able to do it in minutes without any hand coding.

Thanks again


M Shariq 2011-07-03 07:03:16

how to Move Tools below when expender Expend-Down. Also Reset when Expender Close.


CAG 2011-08-05 09:08:49

Nice work Simeon.

Also, AdamH’s suggestion is a good one, else the toggle button is unresponsive when clicking on the portion of the button that is at the same level as the window border.


CAG 2011-08-05 09:10:58

Although I did have trouble getting AdamH’s version working (references some custom bitmap for the button, still got xaml errors when removed that), and had to use TimothyP’s.


Simeon 2011-08-05 09:24:53

Glad you found this useful.


F.X 2011-10-27 10:02:42

Hello ! This is exactly what I am trying to do.

In order to try it, I copied the code, and put it in my project via

project > add item > new item > resource Dictionary

I then added the dictionary to my App.xaml

Lastly, I cerated an expander using

Expander Style="{StaticResource GroupBoxExpander}" Header="Scripts" Height="100" HorizontalAlignment="Left" Margin="13,540,0,0" Name="expander1" VerticalAlignment="Top" Width="453">

When I try to run my application, I get an exception at “ “ :

"'Set property 'System.Windows.ResourceDictionary.DeferrableContent' threw an exception.' "

How can i get pass this exception ?
Thanks in advance
F.


Simeon 2011-10-27 11:30:37

Did you try Google for that error? Because this site suggests rebuilding your project…