This article presents a configurable WPF tab header control with a close button in each tab, scroll buttons and items that can be rearranged. The tab header control is based on the listbox in horizontal mode. It can easily be combined with other components to create customised tab controls.
Introduction
For many applications, it is desirable to have a tab control with tab items that can be scrolled, rearranged and closed. Fortunately, it is not too hard to use standard WPF components to achieve this goal.
This article presents a tab header control with items that can be scrolled using left and right arrow buttons, and rearranged using simple drag operations. I decided to create a tab header control, rather than a complete tab control, to allow a greater degree of customization. It is very easy to combine the tab header control with other WPF controls to create a complete tab control.
The tab header control includes numerous dependency properties to allow customization, for example, the brushes used for the background of selected and unselected tabs.
The sample code includes the following demonstration application:
The top tab control contains four tab items. There are left and right scroll buttons to each side of the tab header. By default, a scroll button is drawn as a simple green triangle. On the far right is a button which presents a pop up menu allowing the user to select one of the tab items as follows:
The middle tab control has the tabs beneath the tab content pane, and a custom template for the scroll buttons.
The bottom tab control has the tabs above the tab content pane, and a custom template for each tab item.
The controls at the bottom of the main window allow the user to configure the font, the tab borders and the background colours. Note that the settings do not apply to the third tab control as that uses a custom item container style.
A key feature of each tab control is the ability to rearrange the tab items by dragging them to a new position.
Background
You will need a good understanding of C# and Windows development and a basic knowledge of WPF.
Using the Code
The first tab control is constructed in XAML from a tab header control, a button and a simple label to display the content of the current tab item. The tab header control is defined as follows:
<wpfcontrollibrary:TabHeaderControl Grid.Row="1" Grid.Column="0"
x:Name="_tabHeader1" ItemsSource="{Binding ListBoxItems}"
SelectedItem="{Binding SelectedHeader, Mode=TwoWay}">
<wpfcontrollibrary:TabHeaderControl.DisplayMemberPath>
HeaderText</wpfcontrollibrary:TabHeaderControl.DisplayMemberPath>
<wpfcontrollibrary:TabHeaderControl.SelectedTabBackground>
<Binding Path="SelectedTabBackground" Mode="TwoWay"/>
</wpfcontrollibrary:TabHeaderControl.SelectedTabBackground>
<wpfcontrollibrary:TabHeaderControl.UnselectedTabBackground>
<Binding Path="UnselectedTabBackground" Mode="TwoWay"/>
</wpfcontrollibrary:TabHeaderControl.UnselectedTabBackground>
<wpfcontrollibrary:TabHeaderControl.SelectedTabBorderThickness>
<Binding Path="SelectedTabBorderThickness_Top"/>
</wpfcontrollibrary:TabHeaderControl.SelectedTabBorderThickness>
<wpfcontrollibrary:TabHeaderControl.SelectedTabForeground>Black
</wpfcontrollibrary:TabHeaderControl.SelectedTabForeground>
<wpfcontrollibrary:TabHeaderControl.UnselectedTabForeground>White
</wpfcontrollibrary:TabHeaderControl.UnselectedTabForeground>
<wpfcontrollibrary:TabHeaderControl.FontSize>
<Binding Path="FontSize"/>
</wpfcontrollibrary:TabHeaderControl.FontSize>
<wpfcontrollibrary:TabHeaderControl.FontFamily>
<Binding Path="FontFamily"/>
</wpfcontrollibrary:TabHeaderControl.FontFamily>
<wpfcontrollibrary:TabHeaderControl.DisabledArrowBrush>Transparent
</wpfcontrollibrary:TabHeaderControl.DisabledArrowBrush>
</wpfcontrollibrary:TabHeaderControl>
Most of the above should be self explanatory. The tab list is populated by assigning an observable collection of TabHeaderItem
instances to the ItemsSource
property. The TabHeaderItem
class is defined as follows:
class TabHeaderItem
{
public string Label { get; set; }
public int ID { get; set; }
public string HeaderText
{
get
{
return Label + " : " + ID;
}
}
}
The DisplayMemberPath
property behaves in exactly the same manner as the DisplayMemberPath
property for the ListBox
. It is set to "HeaderText
", hence each tab displays the text returned by the HeaderText
property of the associated TabHeaderItem
instance.
There are numerous properties which control the appearance of tab items. Thus, the UnselectedTabBackground
property defines the background brush for an unselected tab.
The second tab control is similar except that the tab header items are below the tab content, and the scroll buttons have been restyled.
The third tab control is the most interesting as it overrides the ItemContainerStyle
property of the TabHeaderControl
. This allows for a full customization of the tab appearance and functionality. In this case, most of the properties such as UnselectedTabBackground
are not used. The example defines a tab item with a text string and a close button, with the close functionality implemented in the code behind. The ItemContainerStyle
property is defined as follows:
<wpfcontrollibrary:TabHeaderControl Grid.Row="7" Grid.Column="0"
x:Name="_tabHeader3" ItemsSource="{Binding ListBoxItems}"
SelectedItem="{Binding SelectedHeader, Mode=TwoWay}">
<wpfcontrollibrary:TabHeaderControl.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="FrameworkElement.Margin" Value="0"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" />
<EventSetter Event="PreviewMouseLeftButtonDown"
Handler="ListBoxItem_PreviewMouseLeftButtonDown" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border Background="{TemplateBinding Background}"
Padding="4"
SnapsToDevicePixels="true" BorderThickness="0"
CornerRadius="20,10,0,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="2"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="2"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="1" FontSize="14"
Foreground="{TemplateBinding Foreground}"
Background="{TemplateBinding Background}"
Width="Auto" Padding="2,0,2,0" Margin="0"
VerticalAlignment="Center">
<TextBlock>
<Run Text="{Binding Label}"/><Run Text=" ("/>
<Run Text="{Binding ID}"/><Run Text=")"/>
</TextBlock>
</Label>
<Button Grid.Column="3" Width="20" Height="20"
Content="X" FontSize="12"
Background="{TemplateBinding Background}"
Foreground="{TemplateBinding Foreground}"
BorderThickness="0" Click="Button_Click"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="Orange"/>
<Setter Property="Foreground" Value="Black"/>
</Trigger>
<Trigger Property="IsSelected" Value="False">
<Setter Property="Background" Value="DarkSlateBlue"/>
<Setter Property="Foreground" Value="White"/>
</Trigger>
</Style.Triggers>
</Style>
</wpfcontrollibrary:TabHeaderControl.ItemContainerStyle>
<wpfcontrollibrary:TabHeaderControl.FontFamily>
<Binding Path="FontFamily"/>
</wpfcontrollibrary:TabHeaderControl.FontFamily>
</wpfcontrollibrary:TabHeaderControl>
Triggers are used to style the tab when selected and not selected.
An interesting point to note is the following extract from the data template:
<EventSetter Event="PreviewMouseLeftButtonDown"
Handler="ListBoxItem_PreviewMouseLeftButtonDown" />
The ListBoxItem_PreviewMouseLeftButtonDown
handler is invoked before the button click event. The sender argument is the ListBoxItem
allowing the code behind to determine the index of the tab item associated with the subsequent button press. The handler code is as follows:
private void ListBoxItem_PreviewMouseLeftButtonDown
(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
System.Windows.Controls.ListBoxItem listBoxItem =
sender as System.Windows.Controls.ListBoxItem;
if (listBoxItem == null)
{
return;
}
TabHeaderItem tabHeaderItem = listBoxItem.DataContext as TabHeaderItem;
if (tabHeaderItem == null)
{
return;
}
_listBoxItemIndex = (DataContext as MainWindowModel).ListBoxItems.IndexOf(tabHeaderItem);
}
Thus the _listBoxItemIndex
value defines the index of the list box item associated with the subsequent close button click event.
Customization
The appearance and behaviour of the control can be modified using the following dependency properties:
ItemsSource | Gets or sets a collection used to generate the content of the tabs |
SelectedItem | Gets or sets the currently selected item |
SelectedIndex | Gets or sets the index of the currently selected tab or returns -1 if there is no selected tab |
SelectedValue | Gets or sets the currently selected value
|
SelectedValuePath | Gets or sets the path that is used to get the SelectedValue from the SelectedItem |
ArrowTemplate | Gets or sets the control template for the arrow buttons. The right hand button is identical to the left hand button apart from a 180 degree rotation. |
DisplayMemberPath | Gets or sets a path to a value on the source object to serve as the visual representation of the object |
SelectedTabBackground | Gets or sets the brush used for the background of the selected tab |
UnselectedTabBackground | Gets or sets the brush used for the background of the unselected tabs |
SelectedTabBorderThickness | Gets or sets the border thickness for the selected tab |
SelectedTabForeground | Gets or sets the brush used for the foreground of the selected tab |
UnselectedTabForeground | Gets or sets the brush used for the foreground of the unselected tabs |
ItemContainerStyle | Gets or sets the Style that is applied to the container element generated for each item |
| |
The ItemContainerStyle
is the most powerful dependency property as it allows for a full customization of each tab item. The demonstration application includes a tab header control with a custom ItemContainerStyle
.
In addition, text displayed in the control obeys the standard FontSize
, FontFamily
and FontStyle
dependency properties of the user control.
Implementation
The tab header control is implemented using two button controls and a modified listbox with list items arranged horizontally. The listbox is implemented by the TabHeader
class, which derives from the standard ListBox
class, and adds the ability to rearrange items. The code is reasonably straight forward to understand, although working out how to achieve the required functionality was not so easy!
Comments
The tab header control does not support vertical tabs. This would not be too difficult to add by modifying the underlying TabHeader
class.
History
- 30th April 2020: First version
- 1st January 2021: Replaced the example download with a more useful one.