Click here to Skip to main content
15,868,141 members
Articles / Desktop Programming / WPF
Article

Create a WPF Outlook Bar without any Procedural Code

Rate me:
Please Sign up or sign in to vote.
4.78/5 (51 votes)
11 Jul 2008CPOL7 min read 120.4K   3.7K   84   19
In this article we will see the power of XAML and Control Templates by turning a tabcontrol into an OutlookBar without any procedural code
Outlook Bar - Before Styling (tab control)

Outlook Bar - After Styling

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:

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

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

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

A TabControl laid out like an Outlook bar

Let's look at the two key aspects of this template:

XML
<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).

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

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

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

Outlook Bar - Before adding triggers

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:

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

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

XML
<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>
    <!-- More Triggers here... -->
  </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:

XML
<Border BorderBrush="{StaticResource BorderBrush}" 
    BorderThickness="1" SnapsToDevicePixels="True" >
  <DockPanel>
  <StackPanel DockPanel.Dock="Bottom" IsItemsHost="True" />
  <!-- Top label -->
  <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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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:

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

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
United Kingdom United Kingdom

A youthful, enthusiastic (albeit dead) amphibian coder writing software for a financial information firm based in the UK.



Website may come in the future, if I have time...


Comments and Discussions

 
QuestionFloating content location behavior Pin
Ariel Peretz3-Jun-16 1:42
Ariel Peretz3-Jun-16 1:42 
QuestionOriginal Indeed. Pin
Brady Kelly1-Nov-14 4:47
Brady Kelly1-Nov-14 4:47 
AnswerRe: Original Indeed. Pin
Rotted Frog4-Nov-14 9:57
Rotted Frog4-Nov-14 9:57 
GeneralMy vote of 5 Pin
K Claw22-Jul-14 15:58
K Claw22-Jul-14 15:58 
Questionmovement of Tabs Pin
abhik1234-Apr-12 20:23
abhik1234-Apr-12 20:23 
AnswerRe: movement of Tabs Pin
Rotted Frog22-Apr-12 22:45
Rotted Frog22-Apr-12 22:45 
GeneralUnfortunate Code Project Bug Pin
AspDotNetDev16-Jun-11 12:31
protectorAspDotNetDev16-Jun-11 12:31 
GeneralMy vote of 5 Pin
Danil Yelizarov17-Dec-10 3:28
Danil Yelizarov17-Dec-10 3:28 
QuestionHow to set TabControl top label text same as selected TabItem Header? Pin
CalinaB2-Feb-10 11:38
CalinaB2-Feb-10 11:38 
AnswerRe: How to set TabControl top label text same as selected TabItem Header? Pin
Rotted Frog6-Feb-10 14:00
Rotted Frog6-Feb-10 14:00 
QuestionBinding? Pin
Ken Hadden21-Dec-09 13:41
Ken Hadden21-Dec-09 13:41 
AnswerRe: Binding? Pin
Rotted Frog22-Jan-10 10:14
Rotted Frog22-Jan-10 10:14 
GeneralGreat article Pin
Member 36764532-Sep-09 1:12
Member 36764532-Sep-09 1:12 
Generalbinding the TabControl title to the selected item Pin
Member 11775243-Sep-08 11:55
Member 11775243-Sep-08 11:55 
QuestionOriginal?? Pin
indyfromoz11-Jul-08 14:16
indyfromoz11-Jul-08 14:16 
AnswerRe: Original?? Pin
Rotted Frog11-Jul-08 14:22
Rotted Frog11-Jul-08 14:22 
GeneralRe: Original?? Pin
jpmik12-Jul-08 8:45
jpmik12-Jul-08 8:45 
GeneralHmmm. Pin
Pete O'Hanlon11-Jul-08 10:09
subeditorPete O'Hanlon11-Jul-08 10:09 
GeneralRe: Hmmm. PinPopular
Rotted Frog11-Jul-08 14:16
Rotted Frog11-Jul-08 14:16 
Darn. I hadn't read that article, but now I see someone beat me to it.

I like my version better though Smile | :)

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.