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

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

  1. 它应该正确使用多个标签页眉行。
  2. 它应该在所有标签项目的末尾
  3. 标签循环应该正常工作( Alt + Tab ),也就是说,应该跳过+标签。
  4. 我不应该修改我绑定的源集合。 也就是说,控制应该是可重用的。
  5. 该解决scheme应该与MVVM一起工作

在这里输入图像说明

在这里输入图像说明

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

我只是寻找这样做的一般方法。

谷歌举了很多例子,但是如果你深入挖掘一点,他们都不会满足以上五点。

使用IEditableCollectionView的几乎完整的解决scheme:

 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 

这几乎是完整的,因为制表符周期不会跳过“+”选项卡,并会显示空的内容(这不是很好,但我可以忍受它,直到一个更好的解决scheme来临…)。

我使用了对选项卡控件模板的修改,并绑定到我的视图模型中的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); } } 

注意:我用StackPanel包装了TabPanel,以便与TabPanel一起关于属性“ TabStripPlacement ”的值翻转“+”button。 没有inheritance和没有代码隐藏在你的看法。

我相信我已经提出了一个完整的解决scheme,我开始使用NVM的解决scheme来创build我的模板。 然后引用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> 

像这样定义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,但是您可以使用TabPanel之后的button将其放到一个StackPanel中,并将button设置为一个标签。

现在button会创build一个新的TabItem(也许是您自定义创build的),然后将其添加到您作为TabControl的Itemssource的Tabs的ObservableCollection中。

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

4)那么,你的TabControl应该使用一个TabItems的ObservableCollection作为Itemssource,当一个新的被添加/删除时被通知

一些代码:

NewTabButton usercontrol .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; } } 

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

对于@ NVM自己的解决scheme,这可能会更好。 但我没有代表评论,所以…

如果您正在尝试使用已接受的解决scheme,而没有获取add命令来触发,那么您可能没有名为“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}}}"/> 

除了NVM的答案。 我不使用这么多模板和select器的NewItemPlaceholder。 更简单的解决scheme,没有空的内容:

  <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父元素,即窗口(按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); }