Le TabControl en WPF

Visual Studio 2010

 

Le TabControl est un contrôle graphique qui peut être utile, mais il arrive que son utilisation puisse être complexe. Nous allons voir dans cet article comment utiliser ce contrôle et comment le styler.

 

Pour commencer

Tout au long de cet article, j’utiliserai le pattern MVVM.

Pour commencer, nous allons simplement insérer un TabControl dans une fenêtre WPF et lui ajouter un ItemsSource que j’utiliserai pour binder des données.

<Window x:Class="TabControlStyle.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <TabControl ItemsSource="{Binding CollectionSource}">

        </TabControl>
    </Grid>
</Window>

En faisant cela, on se retrouvera un contrôle TabControl dans la fenêtre cependant, il aura le style par défaut.

Le container

Nous allons maintenant ajouter du style sur le contrôle. Pour commencer nous allons appliquer un style sur le contenu du TabControl

<Style x:Key="DefaultTabControlItem" TargetType="{x:Type TabControl}">
        <Setter Property="Margin" Value="5,0,0,0" />
        <Setter Property="Template" >
            <Setter.Value>
                <ControlTemplate TargetType="TabControl">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="40"/>
                            <RowDefinition/>
                        </Grid.RowDefinitions>
                        <StackPanel Grid.Row="0">
                            <TabPanel IsItemsHost="True"/>
                        </StackPanel>
                        <Border Grid.Row="1" 
                                BorderBrush="Orange" 
                                BorderThickness="1" 
                                CornerRadius="0,5,5,5" 
                                Margin="0,0,5,5" 
                                Background="#FFDFECF7" 
                                SnapsToDevicePixels="True">
                            <!--Using ContentSource set to SelectedContent we will focus the content of the tabitem -->
                            <ContentPresenter ContentSource="SelectedContent" HorizontalAlignment="Center"/>
                        </Border>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

Ici nous avons créé un style qui cible uniquement les contrôles de type TabControl (TargetType="{x:Type TabControl}") pour ensuite modifier la propriété Template. Nous obtenons ainsi ceci:

TabControlContaineur

TabItem

Maintenant que le contenu est stylé, nous allons modifier l’apparence de l’onglet. Comme précédemment, nous allons créer un style et nous ciblerons seulement les contrôles de type TabItem.

<Style x:Key="DefaultTabItem" TargetType="{x:Type TabItem}">
        <Setter Property="Background" Value="DarkOrchid"/>
        <Setter Property="Height" Value="40"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="TabItem">
                    <Border Name="Border" CornerRadius="5,5,5,5" BorderBrush="Red" BorderThickness="1,1,1,1">
                        <ContentPresenter ContentSource="Header"/>
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter TargetName="Border" Property="Background" Value="Blue"/>
                            <Setter TargetName="Border" Property="Opacity" Value="0.6"/>
                        </Trigger>
                        <Trigger Property="IsSelected" Value="True">
                            <Setter TargetName="Border" Property="Background" Value="LightBlue" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

Les couleurs ne sont certainement pas les plus harmonieuses entre elles, mais pour l’exemple on remarque bien la différence 🙂
Il n’y a dans ce style rien de particulier, le seul point particulier est que lorsque l’utilisateur survole le contrôle à la souris, celui-ci change de couleur, il en va de même pour l’item sélectionné.

Affichage de la donnée

Si jusqu’à présent, le modèle de données n’était pas important, il va prendre ici toute son importance.
Pour le moment partons du principe qu’il se compose d’une liste d’objets. Ces objets possèdent 2 propriétés de type string. Pour la prochaine partie, ces informations sont suffisantes.

<Window x:Class="TabControlStyle.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <TabControl ItemsSource="{Binding CollectionSource}"
                    Style="{StaticResource DefaultTabControlItem}"
                    SelectedIndex="{Binding SelectedIndex}"
                    ItemContainerStyle="{StaticResource DefaultTabItem}">
            <TabControl.ItemTemplate>
                <DataTemplate>
                    <Border>
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="20"/>
                                <RowDefinition Height="15" />
                            </Grid.RowDefinitions>
                            <TextBlock Grid.Row="0" Text="{Binding Libelle}"/>
                            <TextBlock Grid.Row="1" Text="{Binding SousLibelle}"/>
                        </Grid>
                    </Border>
                </DataTemplate>
            </TabControl.ItemTemplate>
            <TabControl.ContentTemplate>
                <DataTemplate>
                    <Border>
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="20"/>
                                <RowDefinition Height="15" />
                            </Grid.RowDefinitions>
                            <TextBlock Grid.Row="0" Text="{Binding Libelle}"/>
                            <TextBlock Grid.Row="1" Text="{Binding SousLibelle}"/>
                        </Grid>
                    </Border>
                </DataTemplate>
            </TabControl.ContentTemplate>
        </TabControl>
    </Grid>
</Window>

On peut voir que j’ai appliqué les deux styles que j’ai définis auparavant. J’aurai pu aussi définir dans un style la mise en forme des ItemTemplate et du ContentTemplate. Ici j’ai choisi de ne pas le faire.

TabControlAffichageDonnees

Jusque-là c’est bien, le contrôle a un fonctionnement normal, mais comment faire si je veux que le contenu afficher change en fonction de l’onglet choisit ? Par exemple une liste dans un cas, un WebBrowser pour un autre etc.

Optimisation de l’affichage des données

Nous allons dans un premier temps créer une interface que l’on nommera IContentTabControl.
Maintenant nous allons créer des nouvelles Vue avec leur Vue-Modèle associé, Vue-Modèle implémentera l’interface définie auparavant (pour l’exemple ce sera TabItemContentView et TabItemContentViewModel). Pour les lier entre eux j’ai modifié le App.xaml de la sorte:

<Application x:Class="TabControlStyle.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml"
             xmlns:TabViewModel="clr-namespace:TabControlStyle.TabItem">
    <Application.Resources>
        <ResourceDictionary>            
            <DataTemplate DataType="{x:Type TabViewModel:TabItemContentViewModel}">
                <TabViewModel:TabItemContentView />
            </DataTemplate>
        </ResourceDictionary> 
    </Application.Resources>
</Application>

Ce code fonctionne de la manière suivante : lorsque je mets dans un DataTemplate un Content qui a pour type un TabItemContentViewModel alors j’obtiens une vue de type TabItemContentView.

Il faut maintenant ajouter une propriété de type IContentInterface à mes objets qui sont utilisés pour le DataBinding.

private IContentInterface contentViewModel;

        public IContentInterface ContentViewModel
        {
            get { return contentViewModel; }
            set { contentViewModel = value; NotifyPropertyChanged("ContentViewModel"); }
        }

Le type de cette propriété sera spécifié dans le constructeur de l’objet.

Enfin pour finir, on va modifier le XAML de la fenêtre principale de la sorte :

<TabControl.ContentTemplate>
                    <DataTemplate>
                        <ContentControl Content="{Binding ContentViewModel}"/>
                    </DataTemplate>
                </TabControl.ContentTemplate>

L’impact ne porte que sur le ContentTemplate du TabControl.

Voilà en faisant de la sorte chaque onglet aura un contenu différent si besoin est.

Problème de navigation entre les onglet

Un petit problème de navigation peut apparaître lorsque le contenu du TabControl a le focus. Par exemple lorsque le contenu du TabControl est un WebBrowser qui contient une application Silverlight. Si l’application Silverlight a le focus, le clic fait pour changer d’onglet sera consommé pour remettre le focus sur le TabItem, il faudrait donc 2 clics pour pouvoir naviguer.
Pour corriger cela, il faut rajouter l’événement PreviewMouseDown sur le TabItem. Cela ce fait comme suit :

<TabControl.ItemTemplate>
                    <DataTemplate>
                        <Border Focusable="True" 
                            PreviewMouseDown="Border_PreviewMouseDown"  
                            Background="Transparent">
                            ...
                        </Border>
                    </DataTemplate>
                </TabControl.ItemTemplate>

Et le code c# associé :

private void Border_PreviewMouseDown(object sender, MouseButtonEventArgs e)
        {
            var uiElement = sender as UIElement;
            if (uiElement != null) uiElement.Focus();
        }

De cette manière lorsque l’utilisateur cliquera sur un onglet, la première chose faite sera de remettre le focus sur l’onglet.

Allons plus loin

Drag and Drop d’onglet

On peut ajouter la possibilité de drag and drop des onglets (pour les réordonner par exemple).

Pour commencer il faut ajouter certains événements sur le TabControl ceci sont : TabItem.PreviewMouseMove et TabItem.Drop

<TabControl ItemsSource="{Binding CollectionSource}"
                    Style="{StaticResource DefaultTabControlItem}"
                    SelectedIndex="{Binding SelectedIndex}"                     
                    ItemContainerStyle="{StaticResource DefaultTabItem}"
                    TabItem.PreviewMouseMove="TabControl_PreviewMouseMove" 
                    TabItem.Drop="TabControl_Drop">

Maintenant les handler

private void TabControl_PreviewMouseMove(object sender, MouseEventArgs e)
        {
            var tabControl = e.Source as TabControl;
            var tabItem = tabControl.ItemContainerGenerator.ContainerFromIndex(tabControl.SelectedIndex);
            if (Mouse.PrimaryDevice.LeftButton == MouseButtonState.Pressed && tabItem!=null)
            {
                DragDrop.DoDragDrop(tabItem, tabItem, DragDropEffects.All);

            }
        }

        private void TabControl_Drop(object sender, DragEventArgs e)
        {
            var tabControl = sender as TabControl;
            try
            {
                UIElement ob = VisualTreeHelper.GetParent((DependencyObject)e.OriginalSource) as UIElement;

                while (typeof(System.Windows.Controls.TabItem) != ob.GetType())
                {
                    ob = VisualTreeHelper.GetParent((DependencyObject)ob) as UIElement;
                }
                System.Windows.Controls.TabItem tabItemTarget = ob as System.Windows.Controls.TabItem;
                var tabItemSource = e.Data.GetData(typeof(System.Windows.Controls.TabItem)) as System.Windows.Controls.TabItem;

                if (!tabItemTarget.Equals(tabItemSource))
                {


                    ObservableCollection<ITabItemViewModel> source = (ObservableCollection<ITabItemViewModel>)tabControl.ItemsSource;

                    int sourceIndex = source.IndexOf(tabItemSource.Header as ITabItemViewModel);
                    int targetIndex = source.IndexOf(tabItemTarget.Header as ITabItemViewModel);

                    ITabItemViewModel tempSource = source.ElementAt(sourceIndex);
                    ITabItemViewModel tempTarget = source.ElementAt(targetIndex);

                    source.Remove(tabItemSource.Header as ITabItemViewModel);
                    source.Insert(targetIndex, tempSource);

                    source.Remove(tabItemTarget.Header as ITabItemViewModel);
                    source.Insert(sourceIndex, tempTarget);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }    

Ce code permet de réorganiser les onglets. Cette manière n’est pas très agréable car on ne “voit” pas l’onglet qui va être déplacé.

Drag and Drop avec Adorner

Comme il ne sert à rien de réinventer la roue, j’ai réutilisé un code que j’ai trouvé ici.

J’ai juste modifié un peu le template qui sera utilisé lors du drag and drop de la sorte :

<DataTemplate x:Key="DragStyle" DataType="{x:Type TabItem}">
        <Border Name="Border" 
                CornerRadius="5,5,0,0" 
                BorderBrush="Red" 
                BorderThickness="1,1,1,1"
                Margin="2,2,2,0"
                Width="150"
                Height="40"
                Background="Aqua"
                Opacity="0.6">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="20"/>
                    <RowDefinition Height="15" />
                </Grid.RowDefinitions>
                <TextBlock Grid.Row="0" Text="{Binding Libelle}"/>
                <TextBlock Grid.Row="1" Text="{Binding SousLibelle}"/>
            </Grid>
        </Border>
    </DataTemplate>

Au niveau de la fenêtre principale, le TabControl est à l’intérieur d’une balise adorner :

<Window x:Class="TabControlStyle.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:dda="clr-namespace:TabControlStyle.DragDropAdorner"
        Title="MainWindow" Height="350" Width="525">
<dda:ItemsControlDragDropAdorner AllowDrop="True" 
                                          ItemType="{x:Type TabItem}"
                                          DataTemplate="{StaticResource DragStyle}">

Cette balise définit le type d’item qui pourra être Drap/Drop ainsi que le template associé lors de cette action.

Nous pouvons donc voir que l’utilisation de ce contrôle peut être utile pour la présentation et l’organisation des informations, cependant il peut parfois être compliqué d’arriver à ses fins.

Nombre de vue : 891

COMMENTAIRES 3 commentaires

  1. gurcan dit :

    Bonjour,
    et merci pour votre tuto.
    J’aurais une question à la hauteur de vos compétences 🙂 :
    comment procède t’on pour détacher des onglets de la fenêtre principal?
    je m’explique, j’ai une application wpf, j’ai des tabitem dans mon tabcontrol et j’aimerai les sortir de la fenêtre principale.
    j’ai trouve dragablz mais j’aimerais m’en passé, cela ne marche pas sur mon appli, les fenêtres sont chargés dynamiquement.
    Merci d’avance !!!

  2. Marc Gosselin dit :

    Bonjour,

    J’ai fait quelques recherches et je suis tombé sur un fil de discussion sur stackoverflow.
    Cela semble répondre à ton besoin.

    http://stackoverflow.com/questions/19595618/drop-a-window-into-another-window

    J’espère que cela t’aidera.

    Marc

  3. Alain Baroghel dit :

    Bonjour,
    débutant en WPF, je tombe sur le problème (qui n’existe pas avec les WinForms) suivant avec un tabcontrol.
    3 onglets :
    – onglet 1 : une grille et 3 lignes de textes de saisie
    – onglet 2 : une grille et 12 lignes de textes ou combobox pour de la saisie
    – onglet 3 : une grille et 5 lignes de textes, checkbox pour de la saisie

    lorsqu’on passe d’un onglet à l’autre, le fond de la fenêtre apparaît en bas d’écran sur les onglets 1 et 3 parce qu’il y a “moins” de lignes visibles.
    Peut-on résoudre ce problème et faire en sorte que le tabcontrol ait TOUJOURS la même hauteur ?
    merci !

AJOUTER UN COMMENTAIRE