带有添加新选项卡按钮 (+) 的 TabControl

Posted

技术标签:

【中文标题】带有添加新选项卡按钮 (+) 的 TabControl【英文标题】:TabControl with Add New Tab Button (+) 【发布时间】:2011-03-28 23:40:24 【问题描述】:

在 WPF 中选项卡控件的选项卡条中的所有选项卡项的末尾添加“+”按钮选项卡的正确方法是什么?

    它应该与多个标签标题行一起正常工作。 它应该在所有标签项的末尾 标签循环应该可以正常工作(Alt + Tab),也就是说,+ 标签应该被跳过。 我不应该修改我绑定到的源集合。也就是说,控件应该是可重复使用的。 该解决方案应该适用于MVVM

更准确地说,该按钮应完全显示为附加的最后一个选项卡,而不是所有选项卡条行右侧某处的单独按钮。

我只是在寻找执行此操作的一般方法。

谷歌举了很多例子,但如果你深入挖掘,没有一个能满足以上五点。

【问题讨论】:

标签属于标题,停止重新添加。 [CTRL]+[TAB] 在应用程序中循环窗口(或选项卡)。不是 [ALT]+[TAB],它会循环应用程序本身 【参考方案1】:

现有的答案对我来说太复杂了,而且我很懒。所以,我尝试实现一个非常简单的想法。

    始终将 [+] 选项卡添加到最后。 选择最后一个选项卡后,将其设为新选项卡,然后添加另一个最后一个选项卡。

这个想法很简单,但是该死的 WPF 很冗长,所以代码变得有点长。但这可能很容易理解......因为即使我也这样做了。

代码隐藏。

public partial class MainWindow : Window

    int TabIndex = 1;
    ObservableCollection<TabVM> Tabs = new ObservableCollection<TabVM>();
    public MainWindow()
    
        InitializeComponent();
        var tab1 = new TabVM()
        
            Header = $"Tab TabIndex",
            Content = new ContentVM("First tab", 1)
        ;
        Tabs.Add(tab1);
        AddNewPlusButton();

        MyTabControl.ItemsSource = Tabs;
        MyTabControl.SelectionChanged += MyTabControl_SelectionChanged;

    

    private void MyTabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
    
        if(e.Source is TabControl)
        
            var pos = MyTabControl.SelectedIndex;
            if (pos!=0 && pos == Tabs.Count-1) //last tab
            
                var tab = Tabs.Last();
                ConvertPlusToNewTab(tab);
                AddNewPlusButton();
            
        
    

    void ConvertPlusToNewTab(TabVM tab)
    
        //Do things to make it a new tab.
        TabIndex++;
        tab.Header = $"Tab TabIndex";
        tab.IsPlaceholder = false;
        tab.Content = new ContentVM("Tab content", TabIndex);
    

    void AddNewPlusButton()
    
        var plusTab = new TabVM()
        
            Header = "+",
            IsPlaceholder = true
        ;
        Tabs.Add(plusTab);
    

    class TabVM:INotifyPropertyChanged
    
        string _Header;
        public string Header
        
            get => _Header;
            set
            
                _Header = value;
                OnPropertyChanged();
            
        

        bool _IsPlaceholder = false;
        public bool IsPlaceholder
        
            get => _IsPlaceholder;
            set
            
                _IsPlaceholder = value;
                OnPropertyChanged();
            
        

        ContentVM _Content = null;
        public ContentVM Content
        
            get => _Content;
            set
            
                _Content = value;
                OnPropertyChanged();
            
        

        public event PropertyChangedEventHandler PropertyChanged;
        void OnPropertyChanged([CallerMemberName] string property = "")
        
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
        
    

    class ContentVM
    
        public ContentVM(string name, int index)
        
            Name = name;
            Index = index;
        
        public string Name  get; set; 
        public int Index  get; set; 
    

    private void OnTabCloseClick(object sender, RoutedEventArgs e)
    
        var tab = (sender as Button).DataContext as TabVM;
        if (Tabs.Count>2)
         
            var index = Tabs.IndexOf(tab);
            if(index==Tabs.Count-2)//last tab before [+]
            
                MyTabControl.SelectedIndex--;
            
            Tabs.RemoveAt(index);
        
    

XAML

<TabControl Name="MyTabControl">
    <TabControl.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Binding Header, Mode=OneWay" />
                <Button Click="OnTabCloseClick" Width="20" Padding="0" Margin="8 0 0 0" Content="X">
                    <Button.Style>
                        <Style TargetType="Button" x:Name="CloseButtonStyle">
                            <Setter Property="Visibility" Value="Visible"/>
                            <Style.Triggers>
                                <DataTrigger Binding="Binding IsPlaceholder" Value="True">
                                    <Setter Property="Visibility" Value="Collapsed"/>
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </Button.Style>
                </Button>
            </StackPanel>
        </DataTemplate>
    </TabControl.ItemTemplate>

    <TabControl.ContentTemplate>
        <DataTemplate>
            <ContentControl>
                <ContentControl.Resources>
                    <ContentControl x:Key="TabContentTemplate">
                        <StackPanel DataContext="Binding Content" Orientation="Vertical">
                            <TextBlock Text="Binding Path=Name"/>
                            <TextBlock Text="Binding Path=Index"/>
                        </StackPanel>
                    </ContentControl>
                </ContentControl.Resources>
                <ContentControl.Style>
                    <Style TargetType="ContentControl">
                        <Style.Triggers>
                            <DataTrigger Binding="Binding IsPlaceholder" Value="True">
                                <Setter Property="Content"
                                        Value="x:Null"/>                                    
                            </DataTrigger>
                            <DataTrigger Binding="Binding IsPlaceholder" Value="False">
                                <Setter Property="Content"
                                        Value="StaticResource TabContentTemplate"/>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </ContentControl.Style>
            </ContentControl>
        </DataTemplate>
    </TabControl.ContentTemplate>
</TabControl>

【讨论】:

XAML 中有没有办法在TabContentTemplate 中设置网格?我这样做了,但是如果我更改其 Text 以反映所有选项卡的控件,则可以像 TextBox 那样对网格进行任何控制。如何在那里添加网格而不将其值反映到其他选项卡?【参考方案2】:

要完成@NVM 给出的答案,您必须添加的是 PreviewMouseDown 事件:

<TabControl PreviewMouseDown="ActionTabs_PreviewMouseDown"
</TabControl>

然后:

private void ActionTabs_PreviewMouseDown(object sender, MouseButtonEventArgs e)

  ouseButtonEventArgs args = e as MouseButtonEventArgs;

  FrameworkElement source = (FrameworkElement)args.OriginalSource;

  if (source.DataContext.ToString() == "NewItemPlaceholder")
  
      e.Handled = true;
  

【讨论】:

【参考方案3】:

除了 NVM 的回答。 我没有为 NewItemPlaceholder 使用太多模板和选择器。没有空内容的更简单的解决方案:

    <TabControl.ItemContainerStyle>
        <Style TargetType="TabItem">
           <Style.Triggers>
              <DataTrigger Binding="Binding" Value="x:Static CollectionView.NewItemPlaceholder">
                 <Setter Property="Template">
                    <Setter.Value>
                       <ControlTemplate>
                          <Button Command="Binding DataContext.AddPageCommand, RelativeSource=RelativeSource AncestorType=x:Type TabControl"
                                  HorizontalContentAlignment="Center" VerticalContentAlignment="Center" ToolTip="Add page" >
                             +
                          </Button>
                       </ControlTemplate>
                    </Setter.Value>
                 </Setter>
              </DataTrigger>
           </Style.Triggers>
        </Style>
     </TabControl.ItemContainerStyle>

Ctrl+Tab 我决定禁用。这并不容易,您应该在父元素上订阅 KeyDown,即 Window(Ctrl+Shift+Tab 也正确处理):

  public View()
  
     InitializeComponent();
     AddHandler(Keyboard.PreviewKeyDownEvent, (KeyEventHandler)controlKeyDownEvent);
  

  private void controlKeyDownEvent(object sender, KeyEventArgs e)
  
     e.Handled = e.Key == Key.Tab && Keyboard.Modifiers.HasFlag(ModifierKeys.Control);
  

【讨论】:

【参考方案4】:

作为对@NVM 自己的解决方案的评论,这可能会更好;但我还没有代表发表评论所以......

如果您尝试使用已接受的解决方案并且没有触发添加命令,那么您可能没有名为“parentUserControl”的用户控件。

您可以按如下方式更改@NVM 的 TabControl 声明以使其工作:

<TabControl x:Name="parentUserControl"
            ItemsSource="Binding Items"
            ItemTemplateSelector="StaticResource headerTemplateSelector"
            ContentTemplateSelector="StaticResource contentTemplateSelector"/>

显然不是一个好名字给标签控件:);但我猜@NVM 将数据上下文进一步连接到他的视觉树到一个元素以匹配名称。

请注意,我个人更喜欢通过更改以下内容来使用相对绑定:

<Button Content="+" 
        Command="Binding ElementName=parentUserControl, 
                          Path=DataContext.NewCommand"/>

到这里:

<Button Content="+" 
        Command="Binding DataContext.NewCommand, 
                          RelativeSource=RelativeSource AncestorType=x:Type TabControl"/>

【讨论】:

【参考方案5】:

我使用了选项卡控件模板的修改并绑定到我的视图模型中的 AddNewItemCommand 命令。 XAML:

<TabControl x:Class="MyNamespace.MyTabView"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            ItemsSource="Binding MyItemSource"
            SelectedIndex="Binding LastSelectedIndex"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Control.Template>
        <ControlTemplate TargetType="x:Type TabControl">
            <Grid ClipToBounds="true"
                  SnapsToDevicePixels="true"
                  KeyboardNavigation.TabNavigation="Local">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition x:Name="ColumnDefinition0" />
                    <ColumnDefinition x:Name="ColumnDefinition1"
                                      Width="0" />
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition x:Name="RowDefinition0"
                                   Height="Auto" />
                    <RowDefinition x:Name="RowDefinition1"
                                   Height="*" />
                </Grid.RowDefinitions>
                <StackPanel Grid.Column="0"
                            Grid.Row="0"
                            Orientation="Horizontal"
                            x:Name="HeaderPanel">
                    <TabPanel x:Name="_HeaderPanel"
                              IsItemsHost="true"
                              Margin="2,2,2,0"
                              KeyboardNavigation.TabIndex="1"
                              Panel.ZIndex="1" />
                    <Button Content="+"
                            Command="Binding AddNewItemCommand" />
                </StackPanel>

                <Border x:Name="ContentPanel"
                        BorderBrush="TemplateBinding BorderBrush"
                        BorderThickness="TemplateBinding BorderThickness"
                        Background="TemplateBinding Background"
                        Grid.Column="0"
                        KeyboardNavigation.DirectionalNavigation="Contained"
                        Grid.Row="1"
                        KeyboardNavigation.TabIndex="2"
                        KeyboardNavigation.TabNavigation="Local">
                    <ContentPresenter x:Name="PART_SelectedContentHost"
                                      ContentSource="SelectedContent"
                                      Margin="TemplateBinding Padding"
                                      SnapsToDevicePixels="TemplateBinding SnapsToDevicePixels" />
                </Border>
            </Grid>
            <ControlTemplate.Triggers>
                <Trigger Property="TabStripPlacement"
                         Value="Bottom">
                    <Setter Property="Grid.Row"
                            TargetName="HeaderPanel"
                            Value="1" />
                    <Setter Property="Grid.Row"
                            TargetName="ContentPanel"
                            Value="0" />
                    <Setter Property="Height"
                            TargetName="RowDefinition0"
                            Value="*" />
                    <Setter Property="Height"
                            TargetName="RowDefinition1"
                            Value="Auto" />
                    <Setter Property="Margin"
                            TargetName="HeaderPanel"
                            Value="2,0,2,2" />
                </Trigger>
                <Trigger Property="TabStripPlacement"
                         Value="Left">
                    <Setter Property="Orientation"
                            TargetName="HeaderPanel"
                            Value="Vertical" />
                    <Setter Property="Grid.Row"
                            TargetName="HeaderPanel"
                            Value="0" />
                    <Setter Property="Grid.Row"
                            TargetName="ContentPanel"
                            Value="0" />
                    <Setter Property="Grid.Column"
                            TargetName="HeaderPanel"
                            Value="0" />
                    <Setter Property="Grid.Column"
                            TargetName="ContentPanel"
                            Value="1" />
                    <Setter Property="Width"
                            TargetName="ColumnDefinition0"
                            Value="Auto" />
                    <Setter Property="Width"
                            TargetName="ColumnDefinition1"
                            Value="*" />
                    <Setter Property="Height"
                            TargetName="RowDefinition0"
                            Value="*" />
                    <Setter Property="Height"
                            TargetName="RowDefinition1"
                            Value="0" />
                    <Setter Property="Margin"
                            TargetName="HeaderPanel"
                            Value="2,2,0,2" />
                </Trigger>
                <Trigger Property="TabStripPlacement"
                         Value="Right">
                    <Setter Property="Orientation"
                            TargetName="HeaderPanel"
                            Value="Vertical" />
                    <Setter Property="Grid.Row"
                            TargetName="HeaderPanel"
                            Value="0" />
                    <Setter Property="Grid.Row"
                            TargetName="ContentPanel"
                            Value="0" />
                    <Setter Property="Grid.Column"
                            TargetName="HeaderPanel"
                            Value="1" />
                    <Setter Property="Grid.Column"
                            TargetName="ContentPanel"
                            Value="0" />
                    <Setter Property="Width"
                            TargetName="ColumnDefinition0"
                            Value="*" />
                    <Setter Property="Width"
                            TargetName="ColumnDefinition1"
                            Value="Auto" />
                    <Setter Property="Height"
                            TargetName="RowDefinition0"
                            Value="*" />
                    <Setter Property="Height"
                            TargetName="RowDefinition1"
                            Value="0" />
                    <Setter Property="Margin"
                            TargetName="HeaderPanel"
                            Value="0,2,2,2" />
                </Trigger>
                <Trigger Property="IsEnabled"
                         Value="false">
                    <Setter Property="Foreground"
                            Value="DynamicResource x:Static SystemColors.GrayTextBrushKey" />
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </Control.Template>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="5" />
                    <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Binding Caption" />
                <Button Content="x"
                        Grid.Column="2"
                        VerticalAlignment="Top"/>
            </Grid>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</TabControl>

相关视图模型中的代码如下所示:

public ICommand AddNewItemCommand

    get
    
        return new DelegateCommand((param) =>
        
            MyItemSource.Add(CreateMyValueViewModel());
        ,
        (param) => MyItemSource != null);
    

注意:关于属性“TabStripPlacement”的值,我用 StackPanel 包裹了 TabPanel 以将“+”按钮与 TabPanel 一起翻转。在您看来,没有继承,也没有 code-behind。

【讨论】:

【参考方案6】:

使用 IEditableCollectionView 的几乎完整的解决方案:

ObservableCollection<ItemVM> _items;
public ObservableCollection<ItemVM> Items

    get
    
        if (_items == null)
        
            _items = new ObservableCollection<ItemVM>();
            var itemsView = (IEditableCollectionView)CollectionViewSource.GetDefaultView(_items);
            itemsView.NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtEnd;
        

        return _items;
    


private DelegateCommand<object> _newCommand;
public DelegateCommand<object> NewCommand

    get
    
        if (_newCommand == null)
        
            _newCommand = new DelegateCommand<object>(New_Execute);
        

        return _newCommand;
    


private void New_Execute(object parameter)

    Items.Add(new ItemVM());

<DataTemplate x:Key="newTabButtonContentTemplate">
    <Grid/>
</DataTemplate>

<DataTemplate x:Key="newTabButtonHeaderTemplate">
    <Button Content="+"
        Command="Binding ElementName=parentUserControl, Path=DataContext.NewCommand"/>
</DataTemplate>

<DataTemplate x:Key="itemContentTemplate">
    <Grid/>
</DataTemplate>

<DataTemplate x:Key="itemHeaderTemplate">
    <TextBlock Text="TabItem_test"/>
</DataTemplate>

<vw:TemplateSelector x:Key="headerTemplateSelector"
                           NewButtonTemplate="StaticResource newTabButtonHeaderTemplate"
                           ItemTemplate="StaticResource itemHeaderTemplate"/>

<vw:TemplateSelector x:Key="contentTemplateSelector"
                            NewButtonTemplate="StaticResource newTabButtonContentTemplate"
                            ItemTemplate="StaticResource itemContentTemplate"/>

<TabControl ItemsSource="Binding Items"
        ItemTemplateSelector="StaticResource headerTemplateSelector"
        ContentTemplateSelector="StaticResource contentTemplateSelector"/>
public class TemplateSelector : DataTemplateSelector

    public DataTemplate ItemTemplate  get; set; 
    public DataTemplate NewButtonTemplate  get; set; 

    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    
        if (item == CollectionView.NewItemPlaceholder)
        
            return NewButtonTemplate;
        
        else
        
            return ItemTemplate;
        
    


Enter code here

几乎完成了,因为标签循环不会跳过“+”标签,并且会显示空白内容(这不是很好,但我可以忍受它,直到出现更好的解决方案......)。

【讨论】:

你应该接受这个作为你的正确答案,这样人们在寻找相同问题的答案时会找到它。顺便说一句,干得好。 我希望有人能提出一个完整的解决方案。但我想我已经等得够久了...... 这是一个很棒的解决方案。非常适合我。 你有没有回来过这个?我很好奇你是否想出了一个完整的解决方案? 对不起,我在这个项目之后停止使用 WPF。那时确实想到了一些东西,但我不再记得了。我想如果你问一个新问题,有人可能会回答这个特定的问题。【参考方案7】:

像这样定义 TabControl 的 ControlTemplate:

 <!-- Sets the look of the Tabcontrol. -->
<Style x:Key="TabControlStyle" TargetType="x:Type TabControl">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="x:Type TabControl">
                <Grid>
                    <!-- Upperrow holds the tabs themselves and lower the content of the tab -->
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="*"/>
                    </Grid.RowDefinitions>

网格中的上一行是 TabPanel,但您可以将其放入 StackPanel 中,在 TabPanel 后面有一个按钮,并将按钮样式设置为选项卡。

现在该按钮将创建一个新的 TabItem(可能是您自定义创建的)并将其添加到您作为 TabControl 的 Itemssource 的 Tabs 的 ObservableCollection 中。

2 & 3) 它应该总是出现在最后,而且它不是一个标签,所以希望它不是标签循环的一部分

4) 好吧,您的 TabControl 应该使用 TabItems 的 ObservableCollection 作为 Itemssource,以便在添加/删除新项目时收到通知

一些代码:

NewTabButton 用户控件 .cs 文件

public partial class NewTabButton : TabItem

    public NewTabButton()
    
        InitializeComponent();

        Header = "+";
    

还有主窗口:

public partial class Window1 : Window

    public ObservableCollection<TabItem> Tabs  get; set; 

    public Window1()
    
        InitializeComponent();

        Tabs = new ObservableCollection<TabItem>();

        for (int i = 0; i < 20; i++)
        
            TabItem tab = new TabItem();
            tab.Header = "TabNumber" + i.ToString();
            Tabs.Add(tab);
        

        Tabs.Add(new NewTabButton());

        theTabs.ItemsSource = Tabs;
    

现在我们需要找到一种方法让它始终显示在右下角,并为其添加事件和样式(加号作为占位符)。

【讨论】:

感谢 Ingo,我刚刚添加了一张图片以使问题更清楚。当我认为有多行标签项时,您的建议将不起作用。 您可以尝试创建一个继承 TabItem 的 UserControls 并在该事件上创建一个事件,当它被选中时,它会创建一个新的 TabItem 并将其放入自身的集合中。那应该可以完美地工作。如果我有时间,我可能会稍后添加代码。 如果您通过 ItemsSource 绑定,则无法在 XAML 中添加额外的 TabItem。如果您正在谈论使用 CompositeCollection,我认为它仅适用于静态 TabItems,并且您无法绑定项目 您可以添加到作为 itemssource 的 ObservableCollection 中,已经完成并且工作正常。我发现这个解决方案的问题是,当有多行时,最后一个 Tab 以上面的行结束,我将添加代码,我们可以从那里开始工作。 啊,对不起,我认为 MVVM 是给定的。根据您的建议,我需要将 UI(TabItem)带入我的 VM,这是不可接受的。【参考方案8】:

我相信我已经想出了一个完整的解决方案,我从 NVM 的解决方案开始创建我的模板。然后参考DataGrid源代码,想出了一个扩展的TabControl,可以添加和删除项。

ExtendedTabControl.cs

public class ExtendedTabControl : TabControl

    public static readonly DependencyProperty CanUserAddTabsProperty = DependencyProperty.Register("CanUserAddTabs", typeof(bool), typeof(ExtendedTabControl), new PropertyMetadata(false, OnCanUserAddTabsChanged, OnCoerceCanUserAddTabs));

    public bool CanUserAddTabs
    
        get  return (bool)GetValue(CanUserAddTabsProperty); 
        set  SetValue(CanUserAddTabsProperty, value); 
    

    public static readonly DependencyProperty CanUserDeleteTabsProperty = DependencyProperty.Register("CanUserDeleteTabs", typeof(bool), typeof(ExtendedTabControl), new PropertyMetadata(true, OnCanUserDeleteTabsChanged, OnCoerceCanUserDeleteTabs));

    public bool CanUserDeleteTabs
    
        get  return (bool)GetValue(CanUserDeleteTabsProperty); 
        set  SetValue(CanUserDeleteTabsProperty, value); 
    

    public static RoutedUICommand DeleteCommand
    
        get  return ApplicationCommands.Delete; 
    

    public static readonly DependencyProperty NewTabCommandProperty = DependencyProperty.Register("NewTabCommand", typeof(ICommand), typeof(ExtendedTabControl));

    public ICommand NewTabCommand
    
        get  return (ICommand)GetValue(NewTabCommandProperty); 
        set  SetValue(NewTabCommandProperty, value); 
    

    private IEditableCollectionView EditableItems
    
        get  return (IEditableCollectionView)Items; 
    

    private bool ItemIsSelected
    
        get
        
            if (this.SelectedItem != CollectionView.NewItemPlaceholder)
                return true;

            return false;
        
    

    private static void OnCanExecuteDelete(object sender, CanExecuteRoutedEventArgs e)
    
        ((ExtendedTabControl)sender).OnCanExecuteDelete(e);
    

    private static void OnCanUserAddTabsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    
        ((ExtendedTabControl)d).UpdateNewItemPlaceholder();
    

    private static void OnCanUserDeleteTabsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    
        // The Delete command needs to have CanExecute run.
        CommandManager.InvalidateRequerySuggested();
    

    private static object OnCoerceCanUserAddTabs(DependencyObject d, object baseValue)
    
        return ((ExtendedTabControl)d).OnCoerceCanUserAddOrDeleteTabs((bool)baseValue, true);
    

    private static object OnCoerceCanUserDeleteTabs(DependencyObject d, object baseValue)
    
        return ((ExtendedTabControl)d).OnCoerceCanUserAddOrDeleteTabs((bool)baseValue, false);
    

    private static void OnExecutedDelete(object sender, ExecutedRoutedEventArgs e)
    
        ((ExtendedTabControl)sender).OnExecutedDelete(e);
    

    private static void OnSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    
        if (e.NewValue == CollectionView.NewItemPlaceholder)
        
            var tc = (ExtendedTabControl)d;

            tc.Items.MoveCurrentTo(e.OldValue);
            tc.Items.Refresh();
        
    

    static ExtendedTabControl()
    
        Type ownerType = typeof(ExtendedTabControl);

        DefaultStyleKeyProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(typeof(ExtendedTabControl)));
        SelectedItemProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(OnSelectionChanged));

        CommandManager.RegisterClassCommandBinding(ownerType, new CommandBinding(DeleteCommand, new ExecutedRoutedEventHandler(OnExecutedDelete), new CanExecuteRoutedEventHandler(OnCanExecuteDelete)));
    

    protected virtual void OnCanExecuteDelete(CanExecuteRoutedEventArgs e)
    
        // User is allowed to delete and there is a selection.
        e.CanExecute = CanUserDeleteTabs && ItemIsSelected; 
        e.Handled = true;
    

    protected virtual void OnExecutedDelete(ExecutedRoutedEventArgs e)
    
        if (ItemIsSelected)
        
            int indexToSelect = -1;

            object currentItem = e.Parameter ?? this.SelectedItem;
            if (currentItem == this.SelectedItem)
                indexToSelect = Math.Max(this.Items.IndexOf(currentItem) - 1, 0);

            if (currentItem != CollectionView.NewItemPlaceholder)
                EditableItems.Remove(currentItem);

            if (indexToSelect != -1)
            
                // This should focus the row and bring it into view. 
                SetCurrentValue(SelectedItemProperty, this.Items[indexToSelect]);
            
        

        e.Handled = true;
    

    protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
    
        base.OnItemsSourceChanged(oldValue, newValue);

        CoerceValue(CanUserAddTabsProperty);
        CoerceValue(CanUserDeleteTabsProperty);

        UpdateNewItemPlaceholder();
    

    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    
        if (Keyboard.FocusedElement is TextBox)
            Keyboard.FocusedElement.RaiseEvent(new RoutedEventArgs(LostFocusEvent));

        base.OnSelectionChanged(e);
    

    private bool OnCoerceCanUserAddOrDeleteTabs(bool baseValue, bool canUserAddTabsProperty)
    
        // Only when the base value is true do we need to validate
        // that the user can actually add or delete rows. 
        if (baseValue)
        
            if (!this.IsEnabled)
            
                // Disabled TabControls cannot be modified. 
                return false;
            
            else
            
                if ((canUserAddTabsProperty && !this.EditableItems.CanAddNew) || (!canUserAddTabsProperty && !this.EditableItems.CanRemove))
                
                    // The collection view does not allow the add or delete action.
                    return false;
                
            
        

        return baseValue;
    

    private void UpdateNewItemPlaceholder()
    
        var editableItems = EditableItems;

        if (CanUserAddTabs)
        
            // NewItemPlaceholderPosition isn't a DP but we want to default to AtEnd instead of None
            // (can only be done when canUserAddRows becomes true).  This may override the users intent
            // to make it None, however they can work around this by resetting it to None after making
            // a change which results in canUserAddRows becoming true.
            if (editableItems.NewItemPlaceholderPosition == NewItemPlaceholderPosition.None)
                editableItems.NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtEnd;
        
        else
        
            if (editableItems.NewItemPlaceholderPosition != NewItemPlaceholderPosition.None)
                editableItems.NewItemPlaceholderPosition = NewItemPlaceholderPosition.None;
        

        // Make sure the newItemPlaceholderRow reflects the correct visiblity 
        TabItem newItemPlaceholderTab = (TabItem)ItemContainerGenerator.ContainerFromItem(CollectionView.NewItemPlaceholder);
        if (newItemPlaceholderTab != null)
            newItemPlaceholderTab.CoerceValue(VisibilityProperty);
    

CustomStyleSelector.cs

internal class CustomStyleSelector : StyleSelector

    public Style NewItemStyle  get; set; 

    public override Style SelectStyle(object item, DependencyObject container)
    
        if (item == CollectionView.NewItemPlaceholder)
            return NewItemStyle;
        else
            return Application.Current.FindResource(typeof(TabItem)) as Style;
    

TemplateSelector.cs

internal class TemplateSelector : DataTemplateSelector

    public DataTemplate ItemTemplate  get; set; 
    public DataTemplate NewItemTemplate  get; set; 

    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    
        if (item == CollectionView.NewItemPlaceholder)
            return NewItemTemplate;
        else
            return ItemTemplate;
    

Generic.xaml

<!-- This style explains how to style a NewItemPlaceholder. -->
<Style x:Key="NewTabItemStyle" TargetType="x:Type TabItem">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="x:Type TabItem">
                <ContentPresenter ContentSource="Header" HorizontalAlignment="Left" />
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<!-- This template explains how to render a tab item with a close button. -->
<DataTemplate x:Key="ClosableTabItemHeader">
    <DockPanel MinWidth="120">
        <Button DockPanel.Dock="Right" Command="ApplicationCommands.Delete" CommandParameter="Binding" Content="X" Cursor="Hand" Focusable="False" FontSize="10" FontWeight="Bold" Height="16" Width="16" />
        <TextBlock Padding="0,0,10,0" Text="Binding DisplayName" VerticalAlignment="Center" />
    </DockPanel>
</DataTemplate>

<!-- This template explains how to render a tab item with a new button. -->
<DataTemplate x:Key="NewTabItemHeader">
    <Button Command="Binding NewTabCommand, RelativeSource=RelativeSource AncestorType=x:Type local:ExtendedTabControl" Content="+" Cursor="Hand" Focusable="False" FontWeight="Bold"
            Width="Binding ActualHeight, RelativeSource=RelativeSource Self"/>
</DataTemplate>

<local:CustomStyleSelector x:Key="StyleSelector" NewItemStyle="StaticResource NewTabItemStyle" />
<local:TemplateSelector x:Key="HeaderTemplateSelector" ItemTemplate="StaticResource ClosableTabItemHeader" NewItemTemplate="StaticResource NewTabItemHeader" />
<Style x:Key="x:Type local:ExtendedTabControl" BasedOn="StaticResource x:Type TabControl" TargetType="x:Type local:ExtendedTabControl">
    <Setter Property="ItemContainerStyleSelector" Value="StaticResource StyleSelector" />
    <Setter Property="ItemTemplateSelector" Value="StaticResource HeaderTemplateSelector" />
</Style>

【讨论】:

哪里有这个项目的演示? 呃……怎么用?尤其是 Generic.XAML 部分。我将所有这些都放入 但什么也没发生。您已经完成了所有这些代码的编写,但很少解释如何使用它。这就像在没有开罐器的情况下给某人罐头食品。

以上是关于带有添加新选项卡按钮 (+) 的 TabControl的主要内容,如果未能解决你的问题,请参考以下文章

JQuery UI Tabs:在第二个位置添加选项卡

从用户控件中导航WPF选项卡控件?

以编程方式将选项卡栏控制器添加到当前 App Flow

带有选项卡式导航的 Cocoa-Touch 后退按钮

如何在 Xamarin.Forms 的 TabbedPage 的选项卡中放置一个按钮?

带有片段的 Android Up 按钮未显示完整片段