WPF 扩展 TabControl 可保存显示的标签页

一 功能描述:

扩展的 TabControl 可保存显示的项目,这样切换选项卡时就不会因卸载和重新加载 VisualTree 而影响性能

csharp 复制代码
 [TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))]
 public class TabControlEx : HandyControl.Controls.TabControl
 {
     private Panel _itemsHolderPanel;

     public TabControlEx()
     {
         // This is necessary so that we get the initial databound selected item
         ItemContainerGenerator.StatusChanged += ItemContainerGeneratorStatusChanged;
     }

     /// <summary>
     /// If containers are done, generate the selected item
     /// </summary>
     /// <param name="sender">The sender.</param>
     /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
     private void ItemContainerGeneratorStatusChanged(object sender, EventArgs e)
     {
         if (ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
         {
             ItemContainerGenerator.StatusChanged -= ItemContainerGeneratorStatusChanged;
             UpdateSelectedItem();
         }
     }

     /// <summary>
     /// Get the ItemsHolder and generate any children
     /// </summary>
     public override void OnApplyTemplate()
     {
         base.OnApplyTemplate();
         _itemsHolderPanel = GetTemplateChild("PART_ItemsHolder") as Panel;
         UpdateSelectedItem();
     }

     /// <summary>
     /// When the items change we remove any generated panel children and add any new ones as necessary
     /// </summary>
     /// <param name="e">The <see cref="NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
     protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
     {
         base.OnItemsChanged(e);

         if (_itemsHolderPanel == null)
         {
             return;
         }

         switch (e.Action)
         {
             case NotifyCollectionChangedAction.Reset:
                 _itemsHolderPanel.Children.Clear();
                 break;

             case NotifyCollectionChangedAction.Add:
             case NotifyCollectionChangedAction.Remove:
                 if (e.OldItems != null)
                 {
                     foreach (var item in e.OldItems)
                     {
                         var cp = FindChildContentPresenter(item);
                         if (cp != null)
                         {
                             _itemsHolderPanel.Children.Remove(cp);
                         }
                     }
                 }

                 // Don't do anything with new items because we don't want to
                 // create visuals that aren't being shown

                 UpdateSelectedItem();
                 break;

             case NotifyCollectionChangedAction.Replace:
                 throw new NotImplementedException("Replace not implemented yet");
         }
     }

     protected override void OnSelectionChanged(SelectionChangedEventArgs e)
     {
         base.OnSelectionChanged(e);
         UpdateSelectedItem();
     }

     private void UpdateSelectedItem()
     {
         if (_itemsHolderPanel == null)
         {
             return;
         }

         // Generate a ContentPresenter if necessary
         var item = GetSelectedTabItem();
         if (item != null)
         {
             CreateChildContentPresenter(item);
         }

         // show the right child
         foreach (ContentPresenter child in _itemsHolderPanel.Children)
         {
             child.Visibility = ((child.Tag as HandyControl.Controls.TabItem).IsSelected) ? Visibility.Visible : Visibility.Collapsed;
         }
     }

     private ContentPresenter CreateChildContentPresenter(object item)
     {
         if (item == null)
         {
             return null;
         }

         var cp = FindChildContentPresenter(item);

         if (cp != null)
         {
             return cp;
         }

         var tabItem = item as HandyControl.Controls.TabItem;
         cp = new ContentPresenter
         {
             Content = (tabItem != null) ? tabItem.Content : item,
             ContentTemplate = this.SelectedContentTemplate,
             ContentTemplateSelector = this.SelectedContentTemplateSelector,
             ContentStringFormat = this.SelectedContentStringFormat,
             Visibility = Visibility.Collapsed,
             Tag = tabItem ?? (this.ItemContainerGenerator.ContainerFromItem(item))
         };
         _itemsHolderPanel.Children.Add(cp);
         return cp;
     }

     private ContentPresenter FindChildContentPresenter(object data)
     {
         if (data is HandyControl.Controls.TabItem)
         {
             data = (data as HandyControl.Controls.TabItem).Content;
         }

         if (data == null)
         {
             return null;
         }

         if (_itemsHolderPanel == null)
         {
             return null;
         }

         foreach (ContentPresenter cp in _itemsHolderPanel.Children)
         {
             if (cp.Content == data)
             {
                 return cp;
             }
         }

         return null;
     }

     protected HandyControl.Controls.TabItem GetSelectedTabItem()
     {
         var selectedItem = SelectedItem;
         if (selectedItem == null)
             return null;

         return selectedItem as HandyControl.Controls.TabItem ?? ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as HandyControl.Controls.TabItem;
     }

     protected override AutomationPeer OnCreateAutomationPeer()
     {
         return new TabControlAutomationPeer(this);
     }
 }

二 使用示例:

注意注释地方和系统样式不一样!
修改内容:系统样式使用ContentPresenter 作为承载容器,切换标签页使会重新加载并且标签页内容会互相影响。这里使用Grid x:Name="PART_ItemsHolder"作为承载容器,内容通过Visibility控制显示!

csharp 复制代码
 <control:TabControlEx ItemsSource="{Binding TabItems, Mode=TwoWay}"
                       SelectedItem="{Binding SelectedTab, Mode=TwoWay}">
	<control:TabControlEx.Style>
		<Style TargetType="control:TabControlEx">
			<Setter Property="Template">
				<Setter.Value>
					<ControlTemplate TargetType="control:TabControlEx">
						<Border BorderBrush="{TemplateBinding BorderBrush}"
								CornerRadius="{TemplateBinding hc:BorderElement.CornerRadius}"
								BorderThickness="{TemplateBinding BorderThickness}"
								Background="{DynamicResource Background2}">
							<Grid Name="templateRoot"
								  ClipToBounds="true"
								  SnapsToDevicePixels="true">
								<Grid.ColumnDefinitions>
									<ColumnDefinition Width="*"
													  x:Name="column1" />
									<ColumnDefinition Width="auto" />
								</Grid.ColumnDefinitions>
								<Grid.RowDefinitions>
									<RowDefinition Height="60" />
									<RowDefinition Height="*" />
								</Grid.RowDefinitions>
								
								<TabPanel Name="PART_HeaderPanel"
										 IsItemsHost="true"
										 ZIndex="1" />
							   
								<Border x:Name="contentPanel"
										Background="{DynamicResource WhiteBackground}"
										Grid.Column="0"
										Grid.ColumnSpan="2"
										Grid.Row="1">
								
									<Grid x:Name="PART_ItemsHolder"
										  SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">	
										<!--<ContentPresenter Name="PART_SelectedContentHost"
														      ContentSource="SelectedContent"
														      Margin="{TemplateBinding Padding}"
														      SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />-->
									</Grid>
								</Border>
							</Grid>
						</Border>
					</ControlTemplate>
				</Setter.Value>
			</Setter>
		</Style>
	</control:TabControlEx.Style>
</control:TabControlEx>

三 原文连接:stackoverflow

四 TabControl 扩展(模板选择器)

本教程基于Prism + HandyControl + .Net Framework 4.8

要手动添加标签页,并且每个标签页不同,可使用模板选择器实现,示例:

这里每个标签页有自己的后台处理逻辑,即FrontPageView.xaml的后台是FrontPageViewModel.cs

注意点:由于标签页有自己的处理逻辑,并且是嵌入在TabControl中,所以
不能在标签页中开启Prism的自动查找功能 即xmlns:prism="http://prismlibrary.com/" prism:ViewModelLocator.AutoWireViewModel="False" 或者 不写这两行代码

csharp 复制代码
<UserControl.Resources>
    <!-- 首页模板 -->
    <DataTemplate x:Key="FrontPageTemplate"
                  DataType="{x:Type pageview:FrontPageViewModel}">
        <pageview:FrontPageView />
    </DataTemplate>

    <!-- 其他页面模板 -->
    <DataTemplate x:Key="WebPageTemplate"
                  DataType="{x:Type pageview:WebPageViewModel}">
        <pageview:WebPageView />
    </DataTemplate>

    <!-- 模板选择器 -->
    <helper:TabContentTemplateSelector x:Key="TabContentTemplateSelector"
                                       FrontPageTemplate="{StaticResource FrontPageTemplate}"
                                       WebPageTemplate="{StaticResource WebPageTemplate}" />
</UserControl.Resources>


<control:TabControlEx ItemsSource="{Binding TabItems, Mode=TwoWay}"
                       SelectedItem="{Binding SelectedTab, Mode=TwoWay}"
                       ContentTemplateSelector="{StaticResource TabContentTemplateSelector}">
</control:TabControlEx>
csharp 复制代码
  internal class TabContentTemplateSelector : DataTemplateSelector
  {
      public DataTemplate FrontPageTemplate { get; set; }
      public DataTemplate WebPageTemplate { get; set; }

      public override DataTemplate SelectTemplate(object item, DependencyObject container)
      {
          if (item is FrontPageViewModel)
              return FrontPageTemplate;

          if (item is WebPageViewModel)
              return WebPageTemplate;

          return base.SelectTemplate(item, container);
      }
  }

TabControlEx 的后台代码:

csharp 复制代码
private ObservableCollection<object> _allTabItems;
public ObservableCollection<object> TabItems
{
    get { return _allTabItems; }
    set { SetProperty(ref _allTabItems, value); }
}

private object _selectedTab;
public object SelectedTab
{
    get { return _selectedTab; }
    set { SetProperty(ref _selectedTab, value); }
}

//添加标签页
private void OnAddTabCommand()
{
	WebPageViewModel webPage = new WebPageViewModel();
	//TODO:
	TabItems.Add(webPage);
}
相关推荐
故渊at11 小时前
第二板块:Android 四大组件标准化学理 | 第十二篇:四大组件全景总结与系统服务(System Server)架构
android·架构·wpf·四大组件·system service
伶俜6614 小时前
# [特殊字符] 零基础学 ArkUI 数据持久化(专题三):5 种存储方案深度对比
学习·华为·wpf·harmonyos
IT策士15 小时前
Redis 从入门到精通:数据结构String 与键管理
数据结构·redis·wpf
AC赳赳老秦15 小时前
技术文章素材收集自动化:用 OpenClaw 自动爬取行业资讯、技术热点、优质文章
运维·开发语言·python·自动化·wpf·deepseek·openclaw
加号316 小时前
【WPF】 Storyboard 故事板动画设计深度解析
wpf
xiaoshuaishuai816 小时前
C# Avalonia 依赖属性与WPF的区别
开发语言·c#·wpf
大G的笔记本1 天前
生产级 Spring Boot 网关简单实现方案
wpf
稷下元歌3 天前
七天学会plc加机器视觉之AI 接入 外设模块开发全详细操作文档(全程配套视频按文档实操)
python·sql·qt·贪心算法·r语言·wpf·时序数据库
happyprince3 天前
11-Hugging Face Transformers 分布式与并行系统深度分析
分布式·c#·wpf
加号33 天前
【WPF】 基于 Canvas 读取并渲染 DXF 文件的技术指南
c#·wpf