Как создать трапециевидные вкладки в элементе управления вкладками WPF - PullRequest
77 голосов
/ 18 февраля 2009

Как создать трапециевидные вкладки в элементе управления вкладками WPF?
Я хотел бы создать непрямоугольные вкладки, которые выглядят как вкладки в Google Chrome или как вкладки в редакторе кода VS 2008.

Может ли это быть сделано со стилями WPF или должно быть нарисовано в коде?

Есть ли пример кода, доступного в интернете?

Edit:

Существует множество примеров, показывающих, как закруглять углы или изменять цвета вкладок, но я не смог найти ни одного, который бы изменял геометрию вкладки, как эти два примера:

VS Вкладки редактора кода 2008 года
VS 2008 Code Editor Tabs


Вкладки Google Chrome
alt text

Вкладки в этих двух примерах не прямоугольники, а трапеции.

Ответы [ 7 ]

103 голосов
/ 05 июля 2009

Я пытался найти некоторые шаблоны управления или решения для этой проблемы в Интернете, но я не нашел никакого «приемлемого» решения для меня. Поэтому я написал это по-своему, и вот пример моей первой (и последней =)) попытки сделать это:

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

Эти два конвертера я написал, чтобы настроить размер вкладки в соответствии с ее содержимым. На самом деле, я сделал объект Path в зависимости от размера контента. Если вам не нужны вкладки различной ширины, вы можете использовать несколько модифицированных копий:

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

экран:

screenshot

пример проекта (vs2010)

33 голосов
/ 20 января 2011

Примечание. Это только приложение к великолепному ответу ладей.

Хотя решение rooks отлично работало во время выполнения для меня, у меня возникли некоторые проблемы при открытии MainWindow на поверхности конструктора WPF VS2010: дизайнер генерировал исключения и не отображал окно. Кроме того, весь ControlTemplate для TabItem в TabControl.xaml содержал синюю линию кривых, и всплывающая подсказка сообщала мне, что произошло исключение NullReferenceException. У меня было такое же поведение при перемещении соответствующего кода в мое приложение. Проблемы были на двух разных машинах, поэтому я считаю, что это не было связано с какими-либо проблемами моей установки.

В случае, если кто-то испытывает те же проблемы, я нашел исправление, так что пример теперь работает и во время выполнения, и в дизайнере:

Первый : заменить в коде 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>

... по ...

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

Секунда : заменить в конце метода Convert класса ContentToPathConverter ...

return ps;

... по ...

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

return geometry;

У меня нет объяснения, почему он работает стабильно в конструкторе, но не в исходном коде грачей.

11 голосов
/ 05 июля 2012

Я только что закончил элемент управления вкладками в стиле Google Chrome для WPF. Вы можете найти проект на https://github.com/realistschuckle/wpfchrometabs и в блогах, описывающих его на

Надеюсь, что это поможет вам лучше понять создание пользовательского элемента управления вкладками с нуля.

8 голосов
/ 03 июля 2009
<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>
5 голосов
/ 03 декабря 2013

Я знаю, что это старый, но я хотел бы предложить:

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

Main:

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());
            }
        }
    }
}
4 голосов
/ 18 февраля 2009

Да, вы можете сделать это - в основном все, что вам нужно сделать, это создать собственный шаблон элемента управления. Проверьте http://www.switchonthecode.com/tutorials/the-wpf-tab-control-inside-and-out для учебника. Просто прибегая к помощи "wpf", "tabcontrol", "shape", вы получаете страницы результатов.

Я сам не пробовал, но вы должны заменить теги в шаблоне на теги, чтобы получить желаемую форму.

1 голос
/ 15 марта 2019

Для наклона к левой и правой кромкам вкладки, здесь есть модификация улучшения Слаумы от до принятого ответа ладьи . Это замена метода Convert класса 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;
    }

Также в ControlTemplate TabControl добавьте левые (и, возможно, правые) поля в контейнер элементов вкладки (добавлено единственное изменение 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"/>

Проблема: в нижней части левого края вкладки наблюдается небольшой визуальный сбой, когда она не выбрана. Я думаю, что это связано с «движением назад», чтобы начать перед началом области вкладок. Или же связано с рисованием линии линии внизу вкладки (которая не знает, что мы начинаем до левого края вкладки «нормальный» прямоугольник).

...