如何创建 TabControl(自定义控件)的可关闭 TabItem?

How to create closable TabItem of TabControl (custom control)?

我的英语水平很差,因为我的母语不是英语。 希望大家理解。

我想创建具有可关闭功能的选项卡控件。 (ClosableTabControl)

ClosableTabControl 必须具有在单击关闭按钮时关闭选项卡项的功能。 另外,我想自动删除与关闭的标签项相关的 ItemsSource。

因此,我想在外部项目中使用 ClosableTabControl,如下所示。

class MainViewModel
{
    public ObservableCollection<DocumentViewModel> Documents {get;}
    ...
}

class DocumentViewModel
{
    public string Title {get;}
    public object Content {get;}
}

<Window DataContext="MainViewModel">
    <ClosableTabControl ItemsSource="Documents"
                        HeaderBinding="{Binding Title}"/>
</Window>

如您所见,它不需要连接关闭命令来删除外部项目中的文档。 此外,它不需要重写 ItemTemplate 来绑定。 (它将解决使用 HeaderBinding 功能) 我觉得上面的自定义控件给外部工程带来了方便

我尝试像上面那样创建控件,但我遇到了如下问题。

1.它无法删除 ClosableTabControl 的 ItemsSource 元素。 (关闭标签项时需要)

2。我不知道如何实现 HeaderBinding 功能。

我应该怎么做才能解决上述问题? 我希望你的帮助。

感谢您的阅读。

这个快速简单的示例扩展了 TabControl 并且还覆盖了 TabControl 的默认值 Style。新的 Style 必须放在 "/Themes/Generic.xaml" 文件中。 Style 覆盖了默认的 TabItem ControlTemplate 并为其添加了一个关闭按钮。

Button.Command 绑定到 ClosableTabControl 的路由命令 CloseTabRoutedCommand
调用后,ClosableTabControl 检查 Items 集合是否通过数据绑定或例如XAML。

如果 TabItem 是通过 ItemsSource (绑定)创建的,那么 ICommand 属性 ClosableTabControl.RemoveItemCommand 将被执行以使视图模型从集合中删除项目。这是为了避免直接从 ItemsSource 获取项目,这会破坏此 属性 的绑定。传递给命令委托的参数是请求删除的选项卡项目的数据模型(在您的类型 DocumentViewModel 的情况下)。
如果 TabItem 是通过 XAML 创建的,则直接删除该项目而不需要来自视图模型的命令。

MainViewModel.cs

class MainViewModel
{
  public ObservableCollection<DocumentViewModel> Documents {get;}

  // The remove command which is bound to the ClosableTabControl RemoveItemCommand property. 
  // In case the TabControl.ItemsSource is data bound,
  // this command will be invoked to remove the tab item
  public ICommand RemoveTabItemCommand => new AsyncRelayCommand<DocumentViewModel>(item => this.Documents.Remove(item));
    ...
}

ClosableTabControl.cs

public class ClosableTabControl : TabControl
{
  public static readonly RoutedUICommand CloseTabRoutedCommand = new RoutedUICommand(
    "Close TabItem and remove item from ItemsSource", 
    nameof(ClosableTabControl.CloseTabRoutedCommand), 
    typeof(ClosableTabControl));

  // Bind this property to a ICommand implementation of the view model
  public static readonly DependencyProperty RemoveItemCommandProperty = DependencyProperty.Register(
    "RemoveItemCommand",
    typeof(ICommand),
    typeof(ClosableTabControl),
    new PropertyMetadata(default(ICommand)));

  public ICommand RemoveItemCommand 
  { 
    get => (ICommand) GetValue(ClosableTabControl.RemoveItemCommandProperty); 
    set => SetValue(ClosableTabControl.RemoveItemCommandProperty, value); 
  }

  static ClosableTabControl()
  {
    // Override the default style. 
    // The new Style must be located in the "/Themes/Generic.xaml" ResourceDictionary
    DefaultStyleKeyProperty.OverrideMetadata(typeof(ClosableTabControl), new FrameworkPropertyMetadata(typeof(ClosableTabControl)));
  }

  public ClosableTabControl()
  {
    this.CommandBindings.Add(
      new CommandBinding(ClosableTabControl.CloseTabRoutedCommand, ExecuteRemoveTab, CanExecuteRemoveTab));
  }

  private void CanExecuteRemoveTab(object sender, CanExecuteRoutedEventArgs e)
  {
    e.CanExecute = e.OriginalSource is FrameworkElement frameworkElement 
                   && this.Items.Contains(frameworkElement.DataContext)
                   || this.Items.Contains(e.Source);
  }

  private void ExecuteRemoveTab(object sender, ExecutedRoutedEventArgs e)
  {
    if (this.ItemsSource == null)
    {
      object tabItemToRemove = e.Source;
      this.Items.Remove(tabItemToRemove);
    }
    else
    {
      object tabItemToRemove = (e.OriginalSource as FrameworkElement).DataContext;
      if (this.RemoveItemCommand?.CanExecute(tabItemToRemove) ?? false)
      {
        this.RemoveItemCommand.Execute(tabItemToRemove);
      }
    }
  }
}

Generic.xaml

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">


  <Style TargetType="{x:Type ClosableTabControl}"
         BasedOn="{StaticResource {x:Type TabControl}}">
    <Setter Property="Background"
            Value="{x:Static SystemColors.ControlBrush}" />

    <!-- Add a close button to the tab header -->
    <Setter Property="ItemContainerStyle">
      <Setter.Value>
        <Style TargetType="TabItem" BasedOn="{StaticResource {x:Type TabItem}}">
          <Setter Property="BorderThickness"
                  Value="1,1,1,0" />
          <Setter Property="Margin"
                  Value="0,2,0,0" />
          <Setter Property="BorderBrush" Value="DimGray" />
          <Setter Property="Background" Value="LightGray" />
          <Setter Property="Template">
            <Setter.Value>
              <ControlTemplate TargetType="TabItem">
                <Border BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        Background="{TemplateBinding Background}">
                  <StackPanel Orientation="Horizontal">
                    <ContentPresenter x:Name="ContentSite"
                                      VerticalAlignment="Center"
                                      HorizontalAlignment="Center"
                                      ContentSource="Header"
                                      Margin="12,2,12,2"
                                      RecognizesAccessKey="True" />
                    <Button Content="X"
                            Command="{x:Static local:ClosableTabControl.CloseTabRoutedCommand}"
                            Height="16"
                            Width="16"
                            VerticalAlignment="Center"
                            VerticalContentAlignment="Center"
                            Margin="4" />
                  </StackPanel>
                </Border>
                <ControlTemplate.Triggers>
                  <Trigger Property="IsSelected" Value="True">
                    <Setter Property="Background"
                            Value="{x:Static SystemColors.ControlBrush}" />
                    <Setter Property="Panel.ZIndex"
                            Value="100" />
                    <Setter Property="Margin"
                            Value="0,0,0,-1" />
                  </Trigger>
                </ControlTemplate.Triggers>
              </ControlTemplate>
            </Setter.Value>
          </Setter>
        </Style>
      </Setter.Value>
    </Setter>

    <!-- Provide a default DataTemplate for the tab header
         This will only work if the data item has a property Title -->
    <Setter Property="ItemTemplate">
        <DataTemplate>
          <TextBlock Text="{Binding Title}"/>
        </DataTemplate>
      </Setter.Value>
    </Setter>

    <!-- Provide a default DataTemplate for the tab content
         This will only work if the data item has a property Content -->
    <Setter Property="ContentTemplate">
      <Setter.Value>
        <DataTemplate>
          <ContentPresenter Content="{Binding Content}" />
        </DataTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>

用法示例

<Window> 
  <Window.DataContext>
    <MainViewModel" x:Name="MainViewModel" />
  </Window.DataContext>

  <ClosableTabControl ItemsSource="{Binding Documents}"
                      RemoveItemCommand="{Binding RemoveTabItemCommand}" />
</Window>

备注

我建议切换 TabItem.Visibility 并将其设置为 Visibility.Collapsed,而不是从 ItemsSource 中删除该项目(这实际上是您的要求) TabItem 从视图。这是一种更直观和灵活的行为(例如,重新打开最近关闭)。因为当用户从视图中删除它并不意味着它也必须从视图模型中删除。如果视图模型决定真的删除数据模型,它可以简单地将其从绑定源集合中删除。这也将消除 ClosableTabControl.RemoveItemCommand 属性 的需要,因为 Visibility can/must 在 ClosableTabControl.

中处理

所以 ClosableTabControl.ExecuteRemoveTab 方法将变为:

private void ExecuteRemoveTab(object sender, ExecutedRoutedEventArgs e)
{
  object tabItemToRemove = this.ItemsSource == null
    ? e.Source
    : (e.OriginalSource as FrameworkElement).DataContext;

  // Select the next tab after the removed tab
  int lastItemIndex = this.Items.Count - 1;
  int nextItemIndex = this.Items.IndexOf(tabItemToRemove) + 1;
  this.SelectedIndex = Math.Min(lastItemIndex, nextItemIndex);

  (this.ItemContainerGenerator.ContainerFromItem(tabItemToRemove) as UIElement).Visibility = Visibility.Collapsed;
}

用法示例

<Window> 
  <Window.DataContext>
    <MainViewModel" x:Name="MainViewModel" />
  </Window.DataContext>

  <ClosableTabControl ItemsSource="{Binding Documents}" />
</Window>