Introduction
One of the great things about WPF is that it separates a control's behaviour from its presentation. You can take any control, and by changing a template and some styles, you can make it look completely different. In this tutorial, we will go through the principles of this, and make ourselves a control template that turns a TabControl
into an Outlook bar straight from Office 2007. This is all pure XAML, no code required!
Background
A basic understanding of XAML and WPF is assumed, including knowledge of the different layout panels, Resources and Binding.
Starting Out
Let's get started by creating our tab control in XAML:
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
<TabControl Name="monkey">
<TabItem Header="Mail" IsSelected="True">
<ListBox BorderThickness="0">
<ListBoxItem>Your mail here.</ListBoxItem>
</ListBox>
</TabItem>
<TabItem Header="Calendar" />
<TabItem Header="Tasks" />
</TabControl>
</Page>
As you can see, what we have is a very basic TabControl
. Nothing special or exciting yet.
A Basic Control Template
Control Templates are what make WPF so powerful. You can think of the control template as the entire "look" of a control. By replacing the default control template, we can completely change how it looks, while leaving the behaviour unchanged. Let's add a new one to our TabControl
:
<Page.Resources>
<ControlTemplate x:Key="OutlookBar" TargetType="{x:Type TabControl}">
<ControlTemplate.Resources>
<SolidColorBrush x:Key="BorderBrush" Color="#6593CF" />
</ControlTemplate.Resources>
<Border BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"
SnapsToDevicePixels="True" >
<DockPanel>
<StackPanel IsItemsHost="True" DockPanel.Dock="Bottom" />
<ContentPresenter Content="{TemplateBinding SelectedContent}" />
</DockPanel>
</Border>
</ControlTemplate>
</Page.Resources>
Now we can wire it up to the TabControl
by setting the Template
property to our new Control Template:
<TabControl Name="monkey" Template="{StaticResource OutlookBar}">
Now our TabControl
has a new control template. If you look at it in Visual Studio or XAMLPad, you see that an amazing transformation has taken place. It has already taken the shape of an Outlook Bar. Admittedly not a fantastically good looking Outlook Bar, but a more or less fully functional one. Run it and try clicking on the buttons/tabs. The content pane will flick between the content on each of the tabs.
Let's look at the two key aspects of this template:
<StackPanel IsItemsHost="True" DockPanel.Dock="Bottom" />
TabControl
descends from ItemsControl
. What this means is that, like a ListBox
, Menu
or TreeView
, it displays a list of items in some way, in this case, TabItems
. In order to get the ControlTemplate
to render the list, we put a container panel in the template and set the IsItemsHost
property to True
. (I've used StackPanel
, but it could just as easily be any other container).
<ContentPresenter Content="{TemplateBinding SelectedContent}" />
We need to display the content of our selected tab in the content pane at the top of our Outlook Bar. Fortunately, there is a dependency property on the TabControl
which allows us to do this - SelectedContent
. To wire this up we using Template Binding. Template Binding wires the template to the properties of the object(s) the template is applied to. The syntax is just {TemplateBinding PropertyName}
. It is a delightfully simple and elegant approach.
Adding Some Style
We have perfected our layout, however our Outlook bar looks like a TabControl
that fell out of the ugly tree, hitting every branch on the way down. We need to make the tabs look like Outlook bar buttons with the appropriate fonts, backgrounds and highlighting. To do this, we need to use styles.
All a style does is set a group of properties on a control. They are designed so you can make a group of controls look or behave the same way, by just changing one property.
Let's add some more resources that we can use for styling.
<ControlTemplate.Resources>
<SolidColorBrush x:Key="CaptionBrush" Color= "#15428B" />
<SolidColorBrush x:Key="BorderBrush" Color="#6593CF" />
<LinearGradientBrush x:Key="LabelBrush" StartPoint="0, 0" EndPoint="0,1">
<GradientStop Color="#E3EFFF" Offset="0" />
<GradientStop Color="#AFD2FF" Offset="1" />
</LinearGradientBrush>
</ControlTemplate.Resources>
Now let's restyle our TabItems
:
<Style TargetType="{x:Type TabItem}">
<Setter Property="Background" Value="{StaticResource ButtonNormalBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Grid Background="{TemplateBinding Background}" MinHeight="32">
<Line Stroke="{StaticResource BorderBrush}" VerticalAlignment="Top"
Stretch="Fill" X2="1" SnapsToDevicePixels="True" />
<ContentPresenter Margin="5,0,5,0" TextBlock.FontFamily="Tahoma"
TextBlock.FontSize="8pt" TextBlock.FontWeight="Bold"
TextBlock.Foreground="{StaticResource CaptionBrush}"
Content="{TemplateBinding Header}" VerticalAlignment="Center"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
For our style, we specify a TargetType
of TabItem
- this means it will affect all TabItems
within the current scope (i.e. those in any TabControl
that use our template.
If we have a look at this point, the tab control will look something like this:
So at this point, our TabControl
is starting to look like an Outlook bar. However, when we hover over a button in Outlook, the button will change colour to indicate it. We also need a way of highlighting which button is selected. The traditional way we would do this in a Windows Form control is by using events. That option is still open to us using WPF, which has a very rich event model. However the goal of this article is to do this without using code, and WPF provides us with a very neat way of doing this without code - triggers.
Pulling the Trigger
You can think of triggers as conditional styles. Let's have a look at a trigger:
<Trigger Property="IsSelected" Value="False">
<Setter Property="TextElement.Foreground" Value="{StaticResource CaptionBrush}" />
</Trigger>
As you can see, this trigger sets the text colour whenever the IsSelected
property is set to false
. It's as easy as that.
You can set triggers to depend on more than one condition, using a MultiTrigger
:
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="False" />
<Condition Property="IsMouseOver" Value="False" />
</MultiTrigger.Conditions>
<MultiTrigger.Setters>
<Setter Property="Background" Value="{StaticResource ButtonNormalBrush}" />
</MultiTrigger.Setters>
</MultiTrigger>
The conditions all need to be satisfied for a trigger to take effect, so for the one above, if the button is not selected and the mouse is not hovering over it, then the background brush is set.
For our TabItem
, we put our triggers inside the ControlTemplate.Triggers
element, like so:
<ControlTemplate TargetType="{x:Type TabItem}">
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="False" />
<Condition Property="IsMouseOver" Value="False" />
</MultiTrigger.Conditions>
<MultiTrigger.Setters>
<Setter Property="Background" Value="{StaticResource ButtonNormalBrush}" />
</MultiTrigger.Setters>
</MultiTrigger>
-->
</ControlTemplate.Triggers>
<Grid Background="{TemplateBinding Background}"
MinHeight="32" SnapsToDevicePixels="True">
<Line Stroke="{StaticResource BorderBrush}"
VerticalAlignment="Top" Stretch="Fill" X2="1" SnapsToDevicePixels="True" />
<ContentPresenter Margin="5,0,5,0" TextBlock.FontFamily="Tahoma"
TextBlock.FontSize="8pt" TextBlock.FontWeight="Bold"
TextBlock.Foreground="{StaticResource CaptionBrush}"
Content="{TemplateBinding Header}" VerticalAlignment="Center"/>
</Grid>
</ControlTemplate>
For clarity, I've not included all the triggers above. The remaining triggers are included in the source file.
At this point, we're almost finished. All that's left is to add in the label that sits at the top, which turns out to be very straightforward:
<Border BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1" SnapsToDevicePixels="True" >
<DockPanel>
<StackPanel DockPanel.Dock="Bottom" IsItemsHost="True" />
-->
<Grid DockPanel.Dock="Top" MinHeight="28"
Background="{StaticResource ButtonNormalBrush}" SnapsToDevicePixels="True">
<TextBlock FontFamily="Tahoma" Foreground="{StaticResource CaptionBrush}"
VerticalAlignment="Center" Margin="5,0" FontSize="18" FontWeight="Bold" />
<Line Stroke="{StaticResource BorderBrush}"
VerticalAlignment="Bottom" X2="1" Stretch="Fill"/>
</Grid>
<ContentPresenter Content="{TemplateBinding SelectedContent}" />
</DockPanel>
</Border>
And with that, we're done!
Where to Go From Here
The UI pedants amongst you (and I include myself in that grouping) will notice a few shortcomings in the model we've just done. I've included comments below and leave them as exercises for the interested reader.
- The original Outlook Bar includes an overflow panel at the bottom, and a gripper you can use to change the number of full size buttons. This is a behaviour modification, and as such, is pretty much impossible to do using pure XAML, which puts it outside the scope of this article. It might be possible to do something with a bit of code and a restyled
ToolBar
(a ToolBar
offers similar functionality). I'd be interested to hear from anyone who tries this.
- In an Outlook Bar, you need to click and release the button before it changes the content pane at the top, however our control changes the pane on mouse down. This is again a result of using a
TabControl
as our base. It might be possible to change this with some code, and again, I'd love to hear from anyone who has got this to work in a TabControl
.
- The text above the content pane is not set. It's easy to wire up an event that can be used to set this from code.
- The best way to finish this off would almost certainly be a custom control based on an
ItemsControl
. You should be able to reuse almost all of the template above, with a few modifications.
- If you want to change the colours, all you need to do is change the Brushes in the
ControlTemplate.Resource
element.
Points of Interest
Shape Elements and the Layout System
The Line
element cause some pain while I was assembling the sample. My initial attempt at getting a line to appear at the top of the button template was completely unsuccessful, and looked something like this:
<Line Stroke="Navy" StrokeThickness="1" />
The layout engine was reserving the space for my line, but not drawing anything. I tried all sorts, until I read the MSDN help on shapes. In order to get any shape to use the whole space reserved for it in the layout engine, you need to set the Stretch
property. That appeared to be just what I needed so I duly added Stretch="Fill"
, but still wasn't helping until I reread the help and it dawned on me.
Shapes use their own layout space, and then setting the Stretch
property means it expands that to fill out the space allotted to it, while respecting the HorizontalAlignment
and VerticalAlignment
properties. However I wasn't setting any of the properties in the layout space of the line, so while it had a layout space, there was nothing being drawn in it, therefore nothing to stretch. One small change made all the difference:
<Line Stroke="Navy" StrokeThickness="1" Stretch="Fill" X2="1">
This drew a line between 0 and 1 in the line's layout space which the Stretch
property expanded to cover the whole width.
Snaps To Device Pixels
You'll notice that I have made heavy use of the SnapsToDevicePixels
property. When you have straight lines that are horizontal or vertical, you'll notice that the antialiasing will often make them look blurred. By using SnapsToDevicePixels
, you can ensure that the edges always end on whole device pixels, so the edges remain sharp, as when using classic GDI style drawing in Windows Forms. For diagonal lines and curves, the normal anti-aliased setting is usually best.
History
- 6th July, 2008 - First version