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

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
  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

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>

House Update – Under Floor Complete

Well the under floor part of the great house DIY of ’07 is done.

In the process we have:

  • moved 2,500 kg’s of dirt and rubble from under the house
  • laid 100m2 of polythene ground sheeting
  • used 180m of tape
  • got a lot of sore muscles from crawling around in 30cm’s of space for days on end

But the floor looks good

Polythene Ground Sheeting - All done picture 1
Polythene Ground Sheeting - All done picture 2

Now on with:

  • laying the floor back down
  • replacing some wall studs
  • putting weatherboards back on outside of house

and it will be done!

Live Spaces Oddness

I keep getting Google Alerts for one of the posts from my old blog at simeonpilgrim.spaces.live.com. The strange thing is that I have turned off access to these pages to the public, only trusted people may view them, yet, nobody is in my trusted list.

So why is Google still finding the page, and telling me about it? If it’s private?

So I then went on a rampage deleting the blog posts from my spaces.live.com, and when the last two were showing, I noticed one was a post I missed moving to this blog.  In fact I had missed both.  So there are two new posts back dated to October 2005.  I wonder how many other posts I missed….

It’s quite odd reading old posts, as much has changed since 2005, and in other fronts little has changed.

I hope you have all had a merry Christmas, and enjoy the New Year!

Mintshot – Mint Tick Game

What are the chances of winning, and when should you stop.

Now that the mint tick game is back, I thought I’d have a play at lunch time. The two auctions I was playing on had a high score of 16 and 17.

After a few games I decided I wanted to know the odds/cost to ‘win’

Turn Ticks Left Crosses Left Chance of Selecting a Tick Chance to be here Every X Turns Cost to to be here
0 22 8 73.33% 100.0000% 1 20
1 21 8 72.41% 73.3333% 2 40
2 20 8 71.43% 53.1034% 2 40
3 19 8 70.37% 37.9310% 3 60
4 18 8 69.23% 26.6922% 4 80
5 17 8 68.00% 18.4792% 6 120
6 16 8 66.67% 12.5659% 8 160
7 15 8 65.22% 8.3772% 12 240
8 14 8 63.64% 5.4634% 19 380
9 13 8 61.90% 3.4767% 29 580
10 12 8 60.00% 2.1523% 47 940
11 11 8 57.89% 1.2914% 78 1,560
12 10 8 55.56% 0.7476% 134 2,680
13 9 8 52.94% 0.4153% 241 4,820
14 8 8 50.00% 0.2199% 455 9,100
15 7 8 46.67% 0.1099% 910 18,200
16 6 8 42.86% 0.0513% 1,950 39,000
17 5 8 38.46% 0.0220% 4,458 90,960
18 4 8 33.33% 0.0085% 11,825 236,500
19 3 8 27.27% 0.0028% 35,473 709,460
20 2 8 20.00% 0.0008% 130,065 2,601,300
21 1 8 11.11% 0.0002% 650,325 13,006,500
22 0 8 0.00% 0.0000% 5,852,925 117,058,500

So to get 16 ticks is a 1 in ~2000 event, and 17 is a 1 in ~4500.

So I then played 100 games, recording the outcomes and got the following histogram:

Tick Count Games Games with >= #Ticks Effective Game Percentage
0 26 100 100.00%
1 25 74 74.00%
2 15 49 49.00%
3 8 34 34.00%
4 13 26 26.00%
5 1 13 13.00%
6 4 12 12.00%
7 4 8 8.00%
8 1 4 4.00%
9 1 3 3.00%
10 1 2 2.00%
11 1 1 1.00%

Which is in-line with the expected chances from above.

So if you are hoping for a lucky brake, good luck….