【问题标题】:How to create trapezoid tabs in WPF tab control如何在 WPF 选项卡控件中创建梯形选项卡
【发布时间】:2009-02-18 16:55:49
【问题描述】:

如何在 WPF 选项卡控件中创建梯形选项卡?
我想创建类似于 Google Chrome 中的选项卡或 VS 2008 代码编辑器中的选项卡的非矩形选项卡。

可以用 WPF 样式完成还是必须在代码中绘制?

网上有代码示例吗?

编辑:

有很多示例显示如何圆角或更改选项卡的颜色,但我找不到任何更改选项卡几何形状的示例,如以下两个示例:

VS 2008 代码编辑器选项卡


Google Chrome 标签页

这两个示例中的选项卡不是矩形,而是梯形。

【问题讨论】:

    标签: wpf xaml tabcontrol


    【解决方案1】:

    我试图在互联网上为这个问题找到一些控制模板或解决方案,但我没有找到任何“可接受”的解决方案。所以我以我的方式写了它,这是我第一次(也是最后一次=)尝试这样做的一个例子:

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

    屏幕:

    sample project(vs2010)

    【讨论】:

    • @rooks:很好的例子,谢谢!它在运行时对我有用,但我在 VS2010 中的 WPF 设计器中遇到问题。设计器因 NullReferenceException 而崩溃,这似乎是由 XAML 中的 Path.Data 元素引起的。当我注释掉这个元素时,设计师是稳定的。你知道如何解决这个问题吗?
    • @rooks:与此同时,我找到了一个修复程序,可以让您的示例也可以在 VS2010 的设计器视图中工作:请参阅我在此线程中的答案。
    • 哇,这正是我在最后一个小时左右试图完成的。把设计留给我说的设计师!如果可以的话,+1 和 100 更多。
    • 必须如何修改转换器才能在左侧应用“右”贝泽,使两边具有相同的“曲线”?
    • @JoanComasFdz - 多年后,I have an answer to left+right edge sslopes。希望能帮助别人。
    【解决方案2】:

    我知道这是旧的,但我想提议:

    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>
    

    视图模型:

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

    主要:

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

    【讨论】:

      【解决方案3】:

      注意:这只是 rooks 出色答案的附录。

      虽然 rooks 的解决方案在运行时对我来说运行良好,但在 VS2010 WPF 设计器界面中打开 MainWindow 时遇到了一些问题:设计器抛出异常并且没有显示窗口。此外,TabControl.xaml 中 TabItem 的整个 ControlTemplate 有一条蓝色曲线,并且工具提示告诉我发生了 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>
      

      第二次:替换在ContentToPathConverter类的Convert方法末尾...

      return ps;
      

      ...通过...

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

      我无法解释为什么这在设计器中运行稳定,但不是 rooks 的原始代码。

      【讨论】:

      • 根据 Slauma 的回答,请参阅 my answer 以倾斜每个选项卡的左右边缘。
      【解决方案4】:
      <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】:

        我刚刚为 WPF 完成了一个类似于 Google Chrome 的选项卡控件。您可以在https://github.com/realistschuckle/wpfchrometabs 找到该项目,并在

        上发布描述它的博客文章

        希望能帮助您更好地了解从头开始构建自定义选项卡控件。

        【讨论】:

          【解决方案6】:

          要倾斜左右标签边缘,这里是将Slauma's enhancement修改为rook's accepted answer。这是 ContentToPathConverter 类的 Convert 方法的替换:

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

          同样在 TabControl 的 ControlTemplate 中,为选项卡项的容器添加左边距(也可以选择右边距)(唯一的变化是添加了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"/>
          

          问题:选项卡左边缘底部有轻微的视觉“故障”,当它不是选定的选项卡时。我认为这与在选项卡区域开始之前开始“倒退”有关。或者与在选项卡底部绘制线条有关(不知道我们在选项卡“正常”矩形的左边缘之前开始)。

          【讨论】:

            【解决方案7】:

            是的,您可以这样做——基本上您所要做的就是制作一个自定义控件模板。查看 http://www.switchonthecode.com/tutorials/the-wpf-tab-control-inside-and-out (Dead link. WaybackMachine redirects here) 教程。只需在谷歌上搜索“wpf”“tabcontrol”“shape”就会出现结果页面。

            我自己没有尝试过,但是您应该可以将模板中的标签替换为标签以获得您想要的形状。

            【讨论】:

            • 我看到了那个例子,还有更多。它们都只是使用圆角并为选项卡应用一些颜色,但它们仍然是矩形选项卡。我想要梯形。
            • 您应该能够将 标签更改为 标签
            • 是的,但是仍然不会像 Chrome 示例中那样重叠。关于如何做到这一点的任何想法?
            • 我最好的猜测是使用元素的边距设置
            猜你喜欢
            • 1970-01-01
            • 2011-01-18
            • 2011-10-24
            • 1970-01-01
            • 1970-01-01
            • 2015-03-17
            • 1970-01-01
            • 1970-01-01
            • 2011-05-14
            相关资源
            最近更新 更多