Cómo crear pestañas trapezoidales en el control de pestañas de WPF

78

¿Cómo crear pestañas trapezoidales en el control de pestañas WPF?
Me gustaría crear pestañas no rectangulares que se vean como pestañas en Google Chrome o como pestañas en el editor de código de VS 2008.

¿Se puede hacer con estilos WPF o se debe dibujar en código?

¿Hay algún ejemplo de código disponible en Internet?

Editar:

Hay muchos ejemplos que muestran cómo redondear las esquinas o cambiar los colores de las pestañas, pero no pude encontrar ninguno que cambie la geometría de la pestaña como estos dos ejemplos:

VS 2008 Code Editor Tabs
Pestañas del editor de código VS 2008


Pestañas de Google Chrome
texto alternativo

Las pestañas en estos dos ejemplos no son rectángulos, sino trapecios.

Zendar
fuente

Respuestas:

104

Intenté encontrar algunas plantillas de control o soluciones para este problema en Internet, pero no encontré ninguna solución "aceptable" para mí. Así que lo escribí a mi manera y aquí hay un ejemplo de mi primer (y último =)) intento de hacerlo:

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

Escribí estos dos convertidores para ajustar el tamaño de la pestaña a su contenido. En realidad, hago un objeto Path según el tamaño del contenido. Si no necesita pestañas con varios anchos, puede usar una copia modificada de esto:

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

pantalla:

captura de pantalla

proyecto de muestra (vs2010)

torres
fuente
3
@rooks: ¡Gran ejemplo, gracias! Funciona en tiempo de ejecución para mí, pero tengo problemas en el diseñador de WPF en VS2010. El diseñador se bloquea con una NullReferenceException que parece ser causada por el Path.Dataelemento en el XAML. Cuando comento este elemento, el diseñador es estable. ¿Tiene alguna idea de cómo se podría solucionar este problema?
Slauma
3
@rooks: Mientras tanto, encontré una solución para que su ejemplo funcione también en la vista de diseñador de VS2010: vea mi respuesta en este hilo.
Slauma
Vaya, esto es exactamente lo que acabo de pasar la última hora tratando de lograr. ¡Deje el diseño para los diseñadores, digo! +1 y 100 más si pudiera.
Ed S.
¿Cómo se deben modificar los convertidores para aplicar el beizer "derecho" a la izquierda, para que ambos lados tengan las mismas "curvas"?
JoanComasFdz
1
@JoanComasFdz: años después, tengo una respuesta a las pendientes del borde izquierdo + derecho . Ojalá ayude a alguien.
ToolmakerSteve
35

Nota: Este es solo un apéndice de la gran respuesta de las torres.

Si bien la solución de rooks funcionaba perfectamente en tiempo de ejecución para mí, tuve algunos problemas al abrir MainWindow en la superficie del diseñador VS2010 WPF: el diseñador arrojó excepciones y no mostró la ventana. Además, todo el ControlTemplate para TabItem en TabControl.xaml tenía una línea ondulada azul y una información sobre herramientas me decía que se produjo una NullReferenceException. Tuve el mismo comportamiento al mover el código relevante a mi aplicación. Los problemas estaban en dos máquinas diferentes, así que creo que no estaba relacionado con ningún problema de mi instalación.

En caso de que alguien experimente los mismos problemas, encontré una solución para que el ejemplo funcione ahora en tiempo de ejecución y también en el diseñador:

Primero : Reemplazar en el código 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>

... por ...

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

Segundo : Reemplazar al final del método Convert de la clase ContentToPathConverter ...

return ps;

... por ...

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

return geometry;

No tengo una explicación de por qué esto funciona de forma estable en el diseñador pero no en el código original de rooks.

Slauma
fuente
Según la respuesta de Slauma, vea mi respuesta para inclinar los bordes izquierdo y derecho de cada pestaña.
ToolmakerSteve
9
<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>
pn.
fuente
5

Sé que esto es antiguo pero me gustaría proponer:

ingrese la descripción de la imagen aquí

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;
            }
        }
    }

Principal:

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());
            }
        }
    }
}
narendra
fuente
4

sí, puedes hacer eso, básicamente todo lo que tienes que hacer es crear una plantilla de control personalizada. Visite http://www.switchonthecode.com/tutorials/the-wpf-tab-control-inside-and-out para ver un tutorial. Con solo buscar en Google "wpf" "tabcontrol" "shape" se muestran páginas de resultados.

No lo he probado yo mismo, pero debería poder reemplazar la (s) etiqueta (s) en la plantilla con etiquetas para obtener la forma que desea.

Muad'Dib
fuente
Vi ese ejemplo y algunos más. Todos usan esquinas redondeadas y aplican algo de color a las pestañas, pero siguen siendo pestañas rectangulares. Quiero trapezoide.
Zendar
debería poder cambiar las etiquetas <border> por etiquetas <polygon>
Muad'Dib
Sí, pero aún así, no habrá superposición como en el ejemplo de Chrome. ¿Alguna idea de cómo hacer eso?
Tom Deleu
1
mi mejor suposición sería jugar con la configuración de márgenes para su elemento
Muad'Dib
1

Para inclinar los bordes de las pestañas izquierdo y derecho , aquí hay una modificación de la mejora de Slauma a la respuesta aceptada de la torre . Este es un reemplazo del método Convert de la clase 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;
    }

También en ControlTemplate de TabControl, agregue márgenes izquierdos (y opcionalmente derechos) al contenedor de los elementos de la pestaña (se agrega el único cambio 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"/>

Problema: hay un pequeño "error" visual en la parte inferior del borde izquierdo de la pestaña, cuando no es la pestaña seleccionada. Creo que está relacionado con "retroceder" para comenzar antes del comienzo del área de pestañas. O bien relacionado con el dibujo de la línea en la parte inferior de la pestaña (que no sabe que comenzamos antes del borde izquierdo del rectángulo "normal" de las pestañas).

ToolmakerSteve
fuente