如何创建 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>
我的英语水平很差,因为我的母语不是英语。 希望大家理解。
我想创建具有可关闭功能的选项卡控件。 (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>