Wednesday, August 12 2009
I was recently working on a particularly frustrating and repetitive issue for WPF developers: Custom Window Chrome. I mention that it is frustrating merely because it's something that seems trivial but is plagued with gotchas and Win32 hacks. Thankfully, some nice people over at Microsoft have put together a library that takes a lot of the pain out of the problem. It does not solve all the problems however, and that is the purpose of this post of course.
There were two annoying problems that I've seen come up again and again with custom window chrome that I decided to finally remedy once and for all:
- Reusing Window components such as Min / Max / Close buttons, etc.
- Getting SizeToContent to work correctly.
The former of the two problems led to a relatively straight-forward custom control with a number of template parts that are available for use. The latter however, was a bit of a tricky problem. You see in the MeasureOverride of the Window it is simple to return the desired size of the child given infinite height and width, but it is impossible to know when the desired height and width of that child changes because it is not a direct child of the Window object itself in the visual tree.
Some sample XAML from the default template of the CustomChromeWindow will illustrate:
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CustomChromeWindow}">
<Canvas x:Name="PART_RootCanvas"
Width="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=ActualWidth}"
Height="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=ActualHeight}">
<Canvas.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=IsActive}" Value="False">
<Setter Property="Canvas.Background"
Value="{Binding Path=InactiveWindowBarBrush, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:CustomChromeWindow}}}" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Canvas.Background"
Value="{Binding Path=ActiveWindowBarBrush, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:CustomChromeWindow}}}" />
</Style>
</Canvas.Style>
<Border x:Name="PART_BackgroundBorder" Background="{x:Null}" BorderBrush="{TemplateBinding BorderBrush}"
Width="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Canvas}}, Path=Width}"
Height="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Canvas}}, Path=Height}">
<Border.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=WindowState}" Value="Maximized">
<Setter Property="Border.BorderThickness" Value="0"/>
</DataTrigger>
</Style.Triggers>
<Setter Property="Border.BorderThickness" Value="2"/>
</Style>
</Border.Style>
<AdornerDecorator x:Name="PART_Adorner" OpacityMask="{x:Null}"
Height="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Canvas}}, Path=Height}"
Width="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Canvas}}, Path=Width}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Height="{TemplateBinding WindowBarHeight}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="25" />
<ColumnDefinition Width="25" />
<ColumnDefinition Width="25" />
</Grid.ColumnDefinitions>
<Thumb x:Name="PART_WindowBarThumb" Style="{TemplateBinding WindowBarStyle}"
Grid.Column="0" Grid.ColumnSpan="4" IsHitTestVisible="False" />
<Button x:Name="PART_MinimizeButton" Grid.Column="1" Content="_"
Style="{TemplateBinding MinimizeButtonStyle}" ext:WindowChrome.HitTestable="True" />
<Button x:Name="PART_MaximizeButton" Grid.Column="2" Content="+"
Style="{TemplateBinding MaximizeButtonStyle}" ext:WindowChrome.HitTestable="True" />
<Button x:Name="PART_CloseButton" Grid.Column="3" Content="x"
Style="{TemplateBinding CloseButtonStyle}" ext:WindowChrome.HitTestable="True" />
</Grid>
<ContentPresenter />
The last part of this snippet is the most important part. The indicates that this is where our content should be inserted. As you can see this is clearly very far down the visual tree from the root of the template. Normally you could simply override OnChildDesiredSizeChanged, but this is only raised for the first direct child. To solve this problem I implemented the SizeToContentDecorator, which simply raises an event whenever the desired size of its child changes. By wrapping our in one of these decorators and making that decorator a template part available to the window .... *presto changeo* SizeToContent works:
<local:SizeToContentDecorator x:Name="PART_SizingDecorator" Grid.Row="1">
<ContentPresenter />
</local:SizeToContentDecorator>
So that's Custom Window Chrome in a nutshell. The code is of course available on GitHub.
Comments
Peter said on Wednesday, August 12, 2009:
Very interesting and amusing subject. I read with great pleasure.