WPF 탭 컨트롤에서 사다리꼴 탭을 만드는 방법


78

WPF 탭 컨트롤에서 사다리꼴 탭을 만드는 방법은 무엇입니까?
Google 크롬의 탭이나 VS 2008의 코드 편집기의 탭처럼 보이는 직사각형이 아닌 탭을 만들고 싶습니다.

WPF 스타일로 수행 할 수 있습니까? 아니면 코드로 그려야합니까?

인터넷에서 사용할 수있는 코드의 예가 있습니까?

편집하다:

모서리를 둥글게하거나 탭의 색상을 변경하는 방법을 보여주는 많은 예제가 있지만 다음 두 예제와 같이 탭의 지오메트리를 변경하는 예제를 찾을 수 없습니다.

VS 2008 코드 편집기 탭
VS 2008 코드 편집기 탭


Google 크롬 탭
대체 텍스트

이 두 예의 탭은 직사각형이 아니라 사다리꼴입니다.

답변:


104

인터넷에서이 문제에 대한 컨트롤 템플릿이나 솔루션을 찾으려고했지만 "용인 할 수있는"솔루션을 찾지 못했습니다. 그래서 나는 그것을 내 방식으로 썼고 여기에 나의 첫 번째 (그리고 마지막 =)) 시도의 예가 있습니다.

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

화면:

스크린 샷

샘플 프로젝트 (vs2010)


3
@rooks : 좋은 예, 감사합니다! 그것은 나를 위해 런타임에 작동하지만 VS2010의 WPF 디자이너에 문제가 있습니다. 디자이너가 Path.DataXAML 의 요소 로 인해 발생한 것으로 보이는 NullReferenceException과 충돌 합니다. 이 요소를 주석 처리하면 디자이너는 안정적입니다. 이 문제를 해결하는 방법에 대한 단서가 있습니까?
Slauma

3
@rooks : 그동안 VS2010의 디자이너보기에서도 예제가 작동하도록 수정하는 방법을 찾았습니다.이 스레드에서 내 대답을 참조하십시오.
Slauma

와우,이게 정확히 제가 지난 1 시간 동안 성취하려고했던 것입니다. 내가 말하는 디자이너를 위해 디자인을 남겨주세요! 내가 할 수 있다면 +1하고 100 개 더.
Ed S.

왼쪽에 "오른쪽"비이 저를 적용하려면 변환기를 어떻게 수정해야하나요? 양쪽에 동일한 "곡선"이 있습니까?
JoanComasFdz

1
@JoanComasFdz-몇 년 후 왼쪽 + 오른쪽 가장자리 슬로프에 대한 답을 얻었습니다 . 누군가를 도울 것입니다.
ToolmakerSteve

35

참고 : 이것은 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의 답변 에 따라 각 탭의 왼쪽 및 오른쪽 가장자리 기울기에 대한 답변 을 참조 하십시오.
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>

5

나는 이것이 오래되었다는 것을 알고 있지만 제안하고 싶습니다.

여기에 이미지 설명 입력

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

본관:

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

예, 그렇게 할 수 있습니다. 기본적으로 사용자 지정 컨트롤 템플릿을 만드는 것뿐입니다. http://www.switchonthecode.com/tutorials/the-wpf-tab-control-inside-and-out 에서 자습서를 확인하십시오 . "wpf" "tabcontrol" "shape"를 검색하면 결과 페이지가 나타납니다.

나는 이것을 직접 시도하지 않았지만 템플릿의 태그를 태그로 대체하여 원하는 모양을 얻을 수 있습니다.


나는 그 예와 더 많은 것을 보았다. 그들은 모두 둥근 모서리를 사용하고 탭에 일부 색상을 적용하지만 여전히 직사각형 탭입니다. 사다리꼴을 원합니다.
zendar

<border> 태그를 <polygon> 태그로 변경할 수 있어야합니다
Muad'Dib

예,하지만 그래도 Chrome 예제와 같이 겹치는 부분은 없습니다. 그 방법에 대한 아이디어가 있습니까?
Tom Deleu

1
내 가장 좋은 추측은 요소의 여백 설정을 사용하는 것입니다
Muad'Dib

1

사면에 모두 왼쪽과 오른쪽 탭 가장자리, 여기의 수정이다 Slauma의 향상루크의 허용 대답은 . 이것은 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"/>

문제 : 선택한 탭이 아닌 경우 탭의 왼쪽 가장자리 하단에 약간의 시각적 "글리치"가 있습니다. 탭 영역이 시작되기 전에 시작하는 것이 "뒤로 가기"와 관련이 있다고 생각합니다. 또는 탭 하단의 선 그리기와 관련이 있습니다 (탭 "일반"직사각형의 왼쪽 가장자리 이전에 시작한다는 것을 알지 못함).

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.