web-dev-qa-db-fra.com

Comment créer des onglets trapézoïdaux dans le contrôle des onglets WPF

Comment créer des onglets trapézoïdaux dans le contrôle des onglets WPF?
Je voudrais créer des onglets non rectangulaires qui ressemblent à des onglets dans Google Chrome ou des onglets similaires dans l'éditeur de code de VS 2008.

Peut-il être fait avec des styles WPF ou doit-il être dessiné en code?

Existe-t-il un exemple de code disponible sur Internet?

Modifier:

Il y a beaucoup d'exemples qui montrent comment arrondir les coins ou changer les couleurs des onglets, mais je n'ai trouvé aucun qui change la géométrie de l'onglet comme ces deux exemples:

Onglets de l'éditeur de code VS 2008
VS 2008 Code Editor Tabs


Google Chrome tabs
alt text

Les onglets de ces deux exemples ne sont pas des rectangles, mais des trapèzes.

78
zendar

J'ai essayé de trouver des modèles de contrôle ou des solutions à ce problème sur Internet, mais je n'ai trouvé aucune solution "acceptable" pour moi. Je l'ai donc écrit à ma manière et voici un exemple de ma première (et dernière =)) tentative de le faire:

<Window x:Class="TabControlTemplate.Window1"
    xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
    xmlns:src="clr-namespace:TabControlTemplate"
    Title="Window1" Width="600" Height="400">
<Window.Background>
    <LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
        <GradientStop Color="#FF3164a5" Offset="1"/>
        <GradientStop Color="#FF8AAED4" Offset="0"/>
    </LinearGradientBrush>
</Window.Background>
<Window.Resources>
    <src:ContentToPathConverter x:Key="content2PathConverter"/>
    <src:ContentToMarginConverter x:Key="content2MarginConverter"/>

    <SolidColorBrush x:Key="BorderBrush" Color="#FFFFFFFF"/>
    <SolidColorBrush x:Key="HoverBrush" Color="#FFFF4500"/>
    <LinearGradientBrush x:Key="TabControlBackgroundBrush" EndPoint="0.5,0" StartPoint="0.5,1">
        <GradientStop Color="#FFa9cde7" Offset="0"/>
        <GradientStop Color="#FFe7f4fc" Offset="0.3"/>
        <GradientStop Color="#FFf2fafd" Offset="0.85"/>
        <GradientStop Color="#FFe4f6fa" Offset="1"/>
    </LinearGradientBrush>
    <LinearGradientBrush x:Key="TabItemPathBrush" StartPoint="0,0" EndPoint="0,1">
        <GradientStop Color="#FF3164a5" Offset="0"/>
        <GradientStop Color="#FFe4f6fa" Offset="1"/>
    </LinearGradientBrush>

    <!-- TabControl style -->
    <Style x:Key="TabControlStyle" TargetType="{x:Type TabControl}">
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="TabControl">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>
                        <Border Grid.Row="1" BorderThickness="2,0,2,2" Panel.ZIndex="2" CornerRadius="0,0,2,2"
                                BorderBrush="{StaticResource BorderBrush}"
                                Background="{StaticResource TabControlBackgroundBrush}">
                            <ContentPresenter ContentSource="SelectedContent"/>
                        </Border>
                        <StackPanel Orientation="Horizontal" Grid.Row="0" Panel.ZIndex="1" IsItemsHost="true"/>
                        <Rectangle Grid.Row="0" Height="2" VerticalAlignment="Bottom"
                                   Fill="{StaticResource BorderBrush}"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <!-- TabItem style -->
    <Style x:Key="{x:Type TabItem}" TargetType="{x:Type TabItem}">
        <Setter Property="SnapsToDevicePixels" Value="True"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="TabItem">
                    <Grid x:Name="grd">
                        <Path x:Name="TabPath" StrokeThickness="2"
                              Margin="{Binding ElementName=TabItemContent, Converter={StaticResource content2MarginConverter}}"
                              Stroke="{StaticResource BorderBrush}"
                              Fill="{StaticResource TabItemPathBrush}">
                            <Path.Data>
                                <PathGeometry>
                                    <PathFigure IsClosed="False" StartPoint="1,0" 
                                                Segments="{Binding ElementName=TabItemContent, Converter={StaticResource content2PathConverter}}">
                                    </PathFigure>
                                </PathGeometry>
                            </Path.Data>
                            <Path.LayoutTransform>
                                <ScaleTransform ScaleY="-1"/>
                            </Path.LayoutTransform>
                        </Path>
                        <Rectangle x:Name="TabItemTopBorder" Height="2" Visibility="Visible"
                                   VerticalAlignment="Bottom" Fill="{StaticResource BorderBrush}"
                                   Margin="{Binding ElementName=TabItemContent, Converter={StaticResource content2MarginConverter}}" />
                        <ContentPresenter x:Name="TabItemContent" ContentSource="Header"
                                          Margin="10,2,10,2" VerticalAlignment="Center"
                                          TextElement.Foreground="#FF000000"/>
                    </Grid>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsMouseOver" Value="True" SourceName="grd">
                            <Setter Property="Stroke" Value="{StaticResource HoverBrush}" TargetName="TabPath"/>
                        </Trigger>
                        <Trigger Property="Selector.IsSelected" Value="True">
                            <Setter Property="Fill" TargetName="TabPath">
                                <Setter.Value>
                                    <SolidColorBrush Color="#FFe4f6fa"/>
                                </Setter.Value>
                            </Setter>
                            <Setter Property="BitmapEffect">
                                <Setter.Value>
                                    <DropShadowBitmapEffect Direction="302" Opacity="0.4" 
                                                        ShadowDepth="2" Softness="0.5"/>
                                </Setter.Value>
                            </Setter>
                            <Setter Property="Panel.ZIndex" Value="2"/>
                            <Setter Property="Visibility" Value="Hidden" TargetName="TabItemTopBorder"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</Window.Resources>
<Grid Margin="20">
    <TabControl Grid.Row="0" Grid.Column="1" Margin="5" TabStripPlacement="Top" 
                Style="{StaticResource TabControlStyle}" FontSize="16">
        <TabItem Header="MainTab">
            <Border Margin="10">
                <TextBlock Text="The quick brown fox jumps over the lazy dog."/>
            </Border>
        </TabItem>
        <TabItem Header="VeryVeryLongTab" />
        <TabItem Header="Tab" />
    </TabControl>
</Grid>
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;

namespace TabControlTemplate
{
public partial class Window1
{
    public Window1()
    {
        InitializeComponent();
    }
}

public class ContentToMarginConverter: IValueConverter
{
    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return new Thickness(0, 0, -((ContentPresenter)value).ActualHeight, 0);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    #endregion
}

public class ContentToPathConverter: IValueConverter
{
    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        var ps = new PathSegmentCollection(4);
        ContentPresenter cp = (ContentPresenter)value;
        double h = cp.ActualHeight > 10 ? 1.4 * cp.ActualHeight : 10;
        double w = cp.ActualWidth > 10 ? 1.25 * cp.ActualWidth : 10;
        ps.Add(new LineSegment(new Point(1, 0.7 * h), true));
        ps.Add(new BezierSegment(new Point(1, 0.9 * h), new Point(0.1 * h, h), new Point(0.3 * h, h), true));
        ps.Add(new LineSegment(new Point(w, h), true));
        ps.Add(new BezierSegment(new Point(w + 0.6 * h, h), new Point(w + h, 0), new Point(w + h * 1.3, 0), true));
        return ps;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    #endregion
}
}

J'ai écrit ces deux convertisseurs pour ajuster la taille des onglets à son contenu. En fait, je crée un objet Path en fonction de la taille du contenu. Si vous n'avez pas besoin d'onglets de différentes largeurs, vous pouvez en utiliser une copie modifiée:

<Style x:Key="tabPath" TargetType="{x:Type Path}">
      <Setter Property="Stroke" Value="Black"/>
      <Setter Property="Data">
          <Setter.Value>
              <PathGeometry Figures="M 0,0 L 0,14 C 0,18 2,20 6,20 L 60,20 C 70,20 80,0 84,0"/>
          </Setter.Value>
      </Setter>
  </Style>

écran:

screenshot

exemple de projet (vs2010)

103
rooks

Remarque: ce n'est qu'une annexe à la bonne réponse de la tour.

Alors que la solution de rooks fonctionnait parfaitement au moment de l'exécution pour moi, j'ai eu quelques problèmes lors de l'ouverture de la fenêtre principale dans la surface du concepteur VS2010 WPF: le concepteur a levé des exceptions et n'a pas affiché la fenêtre. De plus, tout le ControlTemplate pour TabItem dans TabControl.xaml avait une ligne de squiggle bleue et une info-bulle me disait qu'une NullReferenceException s'était produite. J'ai eu le même comportement lors du déplacement du code correspondant dans mon application. Les problèmes étaient sur deux machines différentes, donc je pense que cela n'était lié à aucun problème de mon installation.

Dans le cas où quelqu'un rencontre les mêmes problèmes, j'ai trouvé un correctif pour que l'exemple fonctionne maintenant au moment de l'exécution et dans le concepteur également:

First: Remplacer dans le code TabControl-XAML ...

<Path x:Name="TabPath" StrokeThickness="2"
      Margin="{Binding ElementName=TabItemContent,
               Converter={StaticResource content2MarginConverter}}"
      Stroke="{StaticResource BorderBrush}"
      Fill="{StaticResource TabItemPathBrush}">
    <Path.Data>
        <PathGeometry>
            <PathFigure IsClosed="False" StartPoint="1,0" 
                 Segments="{Binding ElementName=TabItemContent,
                            Converter={StaticResource content2PathConverter}}">
            </PathFigure>
        </PathGeometry>
    </Path.Data>
    <Path.LayoutTransform>
        <ScaleTransform ScaleY="-1"/>
    </Path.LayoutTransform>
</Path>

... par ...

<Path x:Name="TabPath" StrokeThickness="2"
      Margin="{Binding ElementName=TabItemContent,
               Converter={StaticResource content2MarginConverter}}"
      Stroke="{StaticResource BorderBrush}"
      Fill="{StaticResource TabItemPathBrush}"
      Data="{Binding ElementName=TabItemContent,
             Converter={StaticResource content2PathConverter}}">
    <Path.LayoutTransform>
        <ScaleTransform ScaleY="-1"/>
    </Path.LayoutTransform>
</Path>

Second: Remplacer à la fin de la méthode Convert de la classe ContentToPathConverter ...

return ps;

... par ...

PathFigure figure = new PathFigure(new Point(1, 0), ps, false);
PathGeometry geometry = new PathGeometry();
geometry.Figures.Add(figure);

return geometry;

Je n'ai aucune explication pourquoi cela fonctionne stable dans le concepteur mais pas le code original de rooks.

33
Slauma

Je viens de terminer un contrôle d'onglets de type Google Chrome pour WPF. Vous pouvez trouver le projet sur https://github.com/realistschuckle/wpfchrometabs et des articles de blog le décrivant sur

J'espère que cela vous aidera à mieux comprendre la création d'un contrôle d'onglet personnalisé à partir de zéro.

11
realistschuckle
<Grid>
    <Grid.Resources>
        <Style TargetType="{x:Type TabControl}">
            <Setter Property="ItemContainerStyle">
                <Setter.Value>
                    <Style>
                        <Setter Property="Control.Height" Value="20"></Setter>
                        <Setter Property="Control.Template">
                            <Setter.Value>
                                <ControlTemplate TargetType="{x:Type TabItem}">
                                    <Grid Margin="0 0 -10 0">
                                        <Grid.ColumnDefinitions>
                                            <ColumnDefinition Width="10">
                                            </ColumnDefinition>
                                            <ColumnDefinition></ColumnDefinition>
                                            <ColumnDefinition Width="10"></ColumnDefinition>
                                        </Grid.ColumnDefinitions>
                                        <Path Data="M10 0 L 0 20 L 10 20 " Fill="{TemplateBinding Background}" Stroke="Black"></Path>
                                        <Rectangle Fill="{TemplateBinding Background}" Grid.Column="1"></Rectangle>
                                        <Rectangle VerticalAlignment="Top" Height="1" Fill="Black" Grid.Column="1"></Rectangle>
                                        <Rectangle VerticalAlignment="Bottom" Height="1" Fill="Black" Grid.Column="1"></Rectangle>
                                        <ContentPresenter Grid.Column="1" ContentSource="Header" />
                                       <Path Data="M0 20 L 10 20 L0 0" Fill="{TemplateBinding Background}" Grid.Column="2" Stroke="Black"></Path>
                                    </Grid>
                                    <ControlTemplate.Triggers>
                                        <Trigger Property="IsSelected" Value="True">
                                            <Trigger.Setters>
                                                <Setter Property="Background" Value="Beige"></Setter>
                                                <Setter Property="Panel.ZIndex" Value="1"></Setter>
                                            </Trigger.Setters>
                                        </Trigger>
                                        <Trigger Property="IsSelected" Value="False">
                                            <Trigger.Setters>
                                                <Setter Property="Background" Value="LightGray"></Setter>
                                            </Trigger.Setters>
                                        </Trigger>
                                    </ControlTemplate.Triggers>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </Setter.Value>
            </Setter>
        </Style>
    </Grid.Resources>
    <TabControl>
        <TabItem Header="One" ></TabItem>
        <TabItem Header="Two" ></TabItem>
        <TabItem Header="Three" ></TabItem>
    </TabControl>
</Grid>
8
pn.

Je sais que c'est vieux mais j'aimerais proposer:

enter image description here

XAML:

    <Window.Resources>

    <ControlTemplate x:Key="trapezoidTab" TargetType="TabItem">
        <Grid>
            <Polygon Name="Polygon_Part" Points="{Binding TabPolygonPoints}" />
            <ContentPresenter Name="TabContent_Part" Margin="{TemplateBinding Margin}" Panel.ZIndex="100" ContentSource="Header" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Grid>

        <ControlTemplate.Triggers>

            <Trigger Property="IsMouseOver" Value="False">
                <Setter TargetName="Polygon_Part" Property="Stroke" Value="LightGray"/>
                <Setter TargetName="Polygon_Part" Property="Fill" Value="DimGray" />
            </Trigger>

            <Trigger Property="IsMouseOver" Value="True">
                <Setter TargetName="Polygon_Part" Property="Fill" Value="Goldenrod" />
                <Setter TargetName="Polygon_Part" Property="Stroke" Value="LightGray"/>
            </Trigger>

            <Trigger Property="IsSelected" Value="False">
                <Setter Property="Panel.ZIndex" Value="90"/>
            </Trigger>

            <Trigger Property="IsSelected" Value="True">
                <Setter Property="Panel.ZIndex" Value="100"/>
                <Setter TargetName="Polygon_Part" Property="Stroke" Value="LightGray"/>
                <Setter TargetName="Polygon_Part" Property="Fill" Value="LightSlateGray "/>
            </Trigger>


        </ControlTemplate.Triggers>

    </ControlTemplate>

</Window.Resources>

<!-- Test the tabs-->
<TabControl Name="FruitTab">
    <TabItem Header="Apple" Template="{StaticResource trapezoidTab}" />
    <TabItem Margin="-8,0,0,0" Header="Grapefruit" Template="{StaticResource trapezoidTab}" />
    <TabItem Margin="-16,0,0,0" Header="Pear" Template="{StaticResource trapezoidTab}"/>
</TabControl>

ViewModel:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Shapes;
    using System.ComponentModel;
    using System.Globalization;
    using System.Windows.Media;

    namespace TrapezoidTab
    {
        public class TabHeaderViewModel : INotifyPropertyChanged
        {
            public event PropertyChangedEventHandler PropertyChanged;
            private string _tabHeaderText;
            private List<Point> _polygonPoints;
            private PointCollection _pointCollection;

            public TabHeaderViewModel(string tabHeaderText)
            {
                _tabHeaderText = tabHeaderText;
                TabPolygonPoints = GenPolygon();
            }

            public PointCollection TabPolygonPoints
            {
                get { return _pointCollection; }
                set
                {
                    _pointCollection = value;
                    if (PropertyChanged != null)
                        PropertyChanged(this, new PropertyChangedEventArgs("TabPolygonPoints"));
                }
            }

            public string TabHeaderText
            {
                get { return _tabHeaderText; }
                set
                {
                    _tabHeaderText = value;
                    TabPolygonPoints = GenPolygon();
                    if (PropertyChanged != null)
                        PropertyChanged(this, new PropertyChangedEventArgs("TabHeaderText"));
                }
            }

            private PointCollection GenPolygon()
            {
                var w = new FormattedText(_tabHeaderText, CultureInfo.GetCultureInfo("en-us"), FlowDirection.LeftToRight, new Typeface("Tahoma"), 12, Brushes.Black);
                var width = w.Width + 30;

                _polygonPoints = new List<Point>(4);
                _pointCollection = new PointCollection(4);

                _polygonPoints.Add(new Point(2, 21));
                _polygonPoints.Add(new Point(10, 2));
                _polygonPoints.Add(new Point(width, 2));
                _polygonPoints.Add(new Point(width + 8, 21));

                foreach (var point in _polygonPoints)
                    _pointCollection.Add(point);

                return _pointCollection;
            }
        }
    }

Principale:

namespace TrapezoidTab
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            foreach (var obj in FruitTab.Items)
            {
                var tab = obj as TabItem;
                if (tab == null) continue;
                tab.DataContext = new TabHeaderViewModel(tab.Header.ToString());
            }
        }
    }
}
5
narendra

oui, vous pouvez le faire - en gros, tout ce que vous avez à faire est de créer un modèle de contrôle personnalisé. Consultez http://www.switchonthecode.com/tutorials/the-wpf-tab-control-inside-and-out pour un tutoriel. Il suffit de googler "wpf" "tabcontrol" "forme" pour afficher des pages de résultats.

Je n'ai pas essayé cela moi-même, mais vous devriez pouvoir remplacer les balises du modèle par des balises pour obtenir la forme que vous souhaitez.

4
Muad'Dib

Pour incliner les bords des onglets à gauche et à droite , voici une modification de amélioration de Slauma à réponse acceptée de la tour . Il s'agit d'un remplacement de la méthode Convert de la classe ContentToPathConverter:

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        var ps = new PathSegmentCollection(4);
        ContentPresenter cp = (ContentPresenter)value;
        double h = cp.ActualHeight > 10 ? 1.4 * cp.ActualHeight : 10;
        double w = cp.ActualWidth > 10 ? 1.25 * cp.ActualWidth : 10;
        // Smaller unit, so don't need fractional multipliers.
        double u = 0.1 * h;
        // HACK: Start before "normal" start of tab.
        double x0 = -4 * u;
        // end of transition
        double x9 = w + 8 * u;
        // transition width
        double tw = 8 * u;
        // top "radius" (actually, gradualness of curve. Larger value is more rounded.)
        double rt = 5 * u;
        // bottom "radius" (actually, gradualness of curve. Larger value is more rounded.)
        double rb = 3 * u;
        // "(x0, 0)" is start point - defined in PathFigure.
        // Cubic: From previous endpoint, 2 control points + new endpoint.
        ps.Add(new BezierSegment(new Point(x0 + rb, 0), new Point(x0 + tw - rt, h), new Point(x0 + tw, h), true));
        ps.Add(new LineSegment(new Point(x9 - tw, h), true));
        ps.Add(new BezierSegment(new Point(x9 - tw + rt, h), new Point(x9 - rb, 0), new Point(x9, 0), true));

        // "(x0, 0)" is start point.
        PathFigure figure = new PathFigure(new Point(x0, 0), ps, false);
        PathGeometry geometry = new PathGeometry();
        geometry.Figures.Add(figure);

        return geometry;
    }

Toujours dans le ControlTemplate de TabControl, ajoutez des marges gauche (et éventuellement droite) au conteneur des éléments de l'onglet (la seule modification est ajoutée Margin="20,0,20,0"):

<Style x:Key="TabControlStyle" TargetType="{x:Type TabControl}">
    ...
    <Setter Property="Template">
        ...
        <StackPanel Grid.Row="0" Panel.ZIndex="1" Orientation="Horizontal" IsItemsHost="true" Margin="20,0,20,0"/>

Problème: il y a un léger "pépin" visuel en bas du bord gauche de l'onglet, quand ce n'est pas l'onglet sélectionné. Je pense que c'est lié à "revenir en arrière" pour commencer avant le début de la zone d'onglet. Ou bien lié au dessin de la ligne ligne au bas de l'onglet (qui ne sait pas que nous commençons avant le bord gauche des onglets rectangle "normal").

1
ToolmakerSteve