Click here to Skip to main content
15,885,546 members
Articles / Desktop Programming / WPF

A WPF Tab Header Control with Scroll Buttons, Moveable Tab Items and a Close Button in Each Tab

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
30 Apr 2020CPOL5 min read 14.6K   541   9   5
WPF tab header control using two button controls and a modified listbox with list items arranged horizontally
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:

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

C#
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:

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

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

C#
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.

License

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


Written By
United Kingdom United Kingdom
C#/WPF/C++ Windows developer

Comments and Discussions

 
Questionvertical - plz Pin
super_george20-Apr-21 22:55
super_george20-Apr-21 22:55 
QuestionSource Code Pin
johnbillsmith30-Apr-20 20:48
johnbillsmith30-Apr-20 20:48 
AnswerRe: Source Code Pin
Leif Simon Goodwin30-Apr-20 21:08
Leif Simon Goodwin30-Apr-20 21:08 
GeneralRe: Source Code Pin
johnbillsmith30-Apr-20 21:10
johnbillsmith30-Apr-20 21:10 
AnswerRe: Source Code Pin
Leif Simon Goodwin1-Jan-21 0:42
Leif Simon Goodwin1-Jan-21 0:42 
I took a look at this a few days ago and realised it had the wrong example code. I have lost the original, so I recreated it. The new one should be much more useful. Any issues, please let me know and I will sort them out.

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.