mirror of
https://gitee.com/akwkevin/aistudio.-wpf.-diagram
synced 2026-03-03 00:00:57 +08:00
443 lines
16 KiB
C#
443 lines
16 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Specialized;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Controls.Primitives;
|
|
using System.Windows.Input;
|
|
|
|
namespace AIStudio.Wpf.ADiagram.Controls
|
|
{
|
|
[TemplatePart(Name = OverflowButtonKey, Type = typeof(ContextMenuToggleButton))]
|
|
[TemplatePart(Name = HeaderPanelKey, Type = typeof(TabPanel))]
|
|
[TemplatePart(Name = OverflowScrollviewer, Type = typeof(ScrollViewer))]
|
|
[TemplatePart(Name = ScrollButtonLeft, Type = typeof(ButtonBase))]
|
|
[TemplatePart(Name = ScrollButtonRight, Type = typeof(ButtonBase))]
|
|
[TemplatePart(Name = HeaderBorder, Type = typeof(System.Windows.Controls.Border))]
|
|
public class TabControl : System.Windows.Controls.TabControl
|
|
{
|
|
private const string OverflowButtonKey = "PART_OverflowButton";
|
|
|
|
private const string HeaderPanelKey = "PART_HeaderPanel";
|
|
|
|
private const string OverflowScrollviewer = "PART_OverflowScrollviewer";
|
|
|
|
private const string ScrollButtonLeft = "PART_ScrollButtonLeft";
|
|
|
|
private const string ScrollButtonRight = "PART_ScrollButtonRight";
|
|
|
|
private const string HeaderBorder = "PART_HeaderBorder";
|
|
|
|
private ContextMenuToggleButton _buttonOverflow;
|
|
|
|
internal TabPanel HeaderPanel { get; private set; }
|
|
|
|
private ScrollViewer _scrollViewerOverflow;
|
|
|
|
private ButtonBase _buttonScrollLeft;
|
|
|
|
private ButtonBase _buttonScrollRight;
|
|
|
|
private System.Windows.Controls.Border _headerBorder;
|
|
|
|
/// <summary>
|
|
/// 是否为内部操作
|
|
/// </summary>
|
|
internal bool IsInternalAction;
|
|
|
|
/// <summary>
|
|
/// 是否启用动画
|
|
/// </summary>
|
|
public static readonly DependencyProperty IsAnimationEnabledProperty = DependencyProperty.Register(
|
|
"IsAnimationEnabled", typeof(bool), typeof(TabControl), new PropertyMetadata(false));
|
|
|
|
/// <summary>
|
|
/// 是否启用动画
|
|
/// </summary>
|
|
public bool IsAnimationEnabled
|
|
{
|
|
get => (bool)GetValue(IsAnimationEnabledProperty);
|
|
set => SetValue(IsAnimationEnabledProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 是否可以拖动
|
|
/// </summary>
|
|
public static readonly DependencyProperty IsDraggableProperty = DependencyProperty.Register(
|
|
"IsDraggable", typeof(bool), typeof(TabControl), new PropertyMetadata(false));
|
|
|
|
/// <summary>
|
|
/// 是否可以拖动
|
|
/// </summary>
|
|
public bool IsDraggable
|
|
{
|
|
get => (bool)GetValue(IsDraggableProperty);
|
|
set => SetValue(IsDraggableProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 是否显示关闭按钮
|
|
/// </summary>
|
|
public static readonly DependencyProperty ShowCloseButtonProperty = DependencyProperty.RegisterAttached(
|
|
"ShowCloseButton", typeof(bool), typeof(TabControl), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.Inherits));
|
|
|
|
public static void SetShowCloseButton(DependencyObject element, bool value)
|
|
=> element.SetValue(ShowCloseButtonProperty, value);
|
|
|
|
public static bool GetShowCloseButton(DependencyObject element)
|
|
=> (bool) element.GetValue(ShowCloseButtonProperty);
|
|
|
|
/// <summary>
|
|
/// 是否显示关闭按钮
|
|
/// </summary>
|
|
public bool ShowCloseButton
|
|
{
|
|
get => (bool)GetValue(ShowCloseButtonProperty);
|
|
set => SetValue(ShowCloseButtonProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 是否显示上下文菜单
|
|
/// </summary>
|
|
public static readonly DependencyProperty ShowContextMenuProperty = DependencyProperty.RegisterAttached(
|
|
"ShowContextMenu", typeof(bool), typeof(TabControl), new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.Inherits));
|
|
|
|
public static void SetShowContextMenu(DependencyObject element, bool value)
|
|
=> element.SetValue(ShowContextMenuProperty, value);
|
|
|
|
public static bool GetShowContextMenu(DependencyObject element)
|
|
=> (bool) element.GetValue(ShowContextMenuProperty);
|
|
|
|
/// <summary>
|
|
/// 是否显示上下文菜单
|
|
/// </summary>
|
|
public bool ShowContextMenu
|
|
{
|
|
get => (bool)GetValue(ShowContextMenuProperty);
|
|
set => SetValue(ShowContextMenuProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 是否将标签填充
|
|
/// </summary>
|
|
public static readonly DependencyProperty IsTabFillEnabledProperty = DependencyProperty.Register(
|
|
"IsTabFillEnabled", typeof(bool), typeof(TabControl), new PropertyMetadata(false));
|
|
|
|
/// <summary>
|
|
/// 是否将标签填充
|
|
/// </summary>
|
|
public bool IsTabFillEnabled
|
|
{
|
|
get => (bool)GetValue(IsTabFillEnabledProperty);
|
|
set => SetValue(IsTabFillEnabledProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 标签宽度
|
|
/// </summary>
|
|
public static readonly DependencyProperty TabItemWidthProperty = DependencyProperty.Register(
|
|
"TabItemWidth", typeof(double), typeof(TabControl), new PropertyMetadata(200.0));
|
|
|
|
/// <summary>
|
|
/// 标签宽度
|
|
/// </summary>
|
|
public double TabItemWidth
|
|
{
|
|
get => (double)GetValue(TabItemWidthProperty);
|
|
set => SetValue(TabItemWidthProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 标签高度
|
|
/// </summary>
|
|
public static readonly DependencyProperty TabItemHeightProperty = DependencyProperty.Register(
|
|
"TabItemHeight", typeof(double), typeof(TabControl), new PropertyMetadata(30.0));
|
|
|
|
/// <summary>
|
|
/// 标签高度
|
|
/// </summary>
|
|
public double TabItemHeight
|
|
{
|
|
get => (double)GetValue(TabItemHeightProperty);
|
|
set => SetValue(TabItemHeightProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 是否可以滚动
|
|
/// </summary>
|
|
public static readonly DependencyProperty IsScrollableProperty = DependencyProperty.Register(
|
|
"IsScrollable", typeof(bool), typeof(TabControl), new PropertyMetadata(false));
|
|
|
|
/// <summary>
|
|
/// 是否可以滚动
|
|
/// </summary>
|
|
public bool IsScrollable
|
|
{
|
|
get => (bool) GetValue(IsScrollableProperty);
|
|
set => SetValue(IsScrollableProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 是否显示溢出按钮
|
|
/// </summary>
|
|
public static readonly DependencyProperty ShowOverflowButtonProperty = DependencyProperty.Register(
|
|
"ShowOverflowButton", typeof(bool), typeof(TabControl), new PropertyMetadata(true));
|
|
|
|
/// <summary>
|
|
/// 是否显示溢出按钮
|
|
/// </summary>
|
|
public bool ShowOverflowButton
|
|
{
|
|
get => (bool) GetValue(ShowOverflowButtonProperty);
|
|
set => SetValue(ShowOverflowButtonProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 是否显示滚动按钮
|
|
/// </summary>
|
|
public static readonly DependencyProperty ShowScrollButtonProperty = DependencyProperty.Register(
|
|
"ShowScrollButton", typeof(bool), typeof(TabControl), new PropertyMetadata(false));
|
|
|
|
/// <summary>
|
|
/// 是否显示滚动按钮
|
|
/// </summary>
|
|
public bool ShowScrollButton
|
|
{
|
|
get => (bool) GetValue(ShowScrollButtonProperty);
|
|
set => SetValue(ShowScrollButtonProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 可见的标签数量
|
|
/// </summary>
|
|
private int _itemShowCount;
|
|
|
|
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
|
|
{
|
|
base.OnItemsChanged(e);
|
|
|
|
if (HeaderPanel == null)
|
|
{
|
|
IsInternalAction = false;
|
|
return;
|
|
}
|
|
|
|
UpdateOverflowButton();
|
|
|
|
if (IsInternalAction)
|
|
{
|
|
IsInternalAction = false;
|
|
return;
|
|
}
|
|
|
|
if (IsAnimationEnabled)
|
|
{
|
|
HeaderPanel.SetCurrentValue(TabPanel.FluidMoveDurationProperty, new Duration(TimeSpan.FromMilliseconds(200)));
|
|
}
|
|
else
|
|
{
|
|
HeaderPanel.FluidMoveDuration = new Duration(TimeSpan.FromSeconds(0));
|
|
}
|
|
|
|
if (e.Action == NotifyCollectionChangedAction.Add)
|
|
{
|
|
for (var i = 0; i < Items.Count; i++)
|
|
{
|
|
if (!(ItemContainerGenerator.ContainerFromIndex(i) is TabItem item)) return;
|
|
item.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
|
item.TabPanel = HeaderPanel;
|
|
}
|
|
}
|
|
|
|
_headerBorder?.InvalidateMeasure();
|
|
|
|
IsInternalAction = false;
|
|
}
|
|
|
|
public override void OnApplyTemplate()
|
|
{
|
|
if (_buttonOverflow != null)
|
|
{
|
|
if (_buttonOverflow.Menu != null)
|
|
{
|
|
_buttonOverflow.Menu.Closed -= Menu_Closed;
|
|
_buttonOverflow.Menu = null;
|
|
}
|
|
|
|
_buttonOverflow.Click -= ButtonOverflow_Click;
|
|
}
|
|
|
|
if (_buttonScrollLeft != null) _buttonScrollLeft.Click -= ButtonScrollLeft_Click;
|
|
if (_buttonScrollRight != null) _buttonScrollRight.Click -= ButtonScrollRight_Click;
|
|
|
|
base.OnApplyTemplate();
|
|
HeaderPanel = GetTemplateChild(HeaderPanelKey) as TabPanel;
|
|
|
|
if (IsTabFillEnabled) return;
|
|
|
|
_buttonOverflow = GetTemplateChild(OverflowButtonKey) as ContextMenuToggleButton;
|
|
_scrollViewerOverflow = GetTemplateChild(OverflowScrollviewer) as ScrollViewer;
|
|
_buttonScrollLeft = GetTemplateChild(ScrollButtonLeft) as ButtonBase;
|
|
_buttonScrollRight = GetTemplateChild(ScrollButtonRight) as ButtonBase;
|
|
_headerBorder = GetTemplateChild(HeaderBorder) as System.Windows.Controls.Border;
|
|
|
|
if (_buttonScrollLeft != null) _buttonScrollLeft.Click += ButtonScrollLeft_Click;
|
|
if (_buttonScrollRight != null) _buttonScrollRight.Click += ButtonScrollRight_Click;
|
|
|
|
if (_buttonOverflow != null)
|
|
{
|
|
var menu = new System.Windows.Controls.ContextMenu
|
|
{
|
|
Placement = PlacementMode.Bottom,
|
|
PlacementTarget = _buttonOverflow
|
|
};
|
|
menu.Closed += Menu_Closed;
|
|
_buttonOverflow.Menu = menu;
|
|
_buttonOverflow.Click += ButtonOverflow_Click;
|
|
}
|
|
}
|
|
|
|
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
|
|
{
|
|
base.OnRenderSizeChanged(sizeInfo);
|
|
UpdateOverflowButton();
|
|
}
|
|
|
|
private void UpdateOverflowButton()
|
|
{
|
|
if (!IsTabFillEnabled)
|
|
{
|
|
_itemShowCount = (int)(ActualWidth / TabItemWidth);
|
|
if (_buttonOverflow != null)
|
|
{
|
|
_buttonOverflow.Visibility = (ShowOverflowButton && Items.Count > 0 && Items.Count >= _itemShowCount) ? Visibility.Visible : Visibility.Collapsed;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Menu_Closed(object sender, RoutedEventArgs e) => _buttonOverflow.IsChecked = false;
|
|
|
|
private void ButtonScrollRight_Click(object sender, RoutedEventArgs e) =>
|
|
_scrollViewerOverflow.ScrollToHorizontalOffsetInternal(Math.Min(
|
|
_scrollViewerOverflow.CurrentHorizontalOffset + TabItemWidth, _scrollViewerOverflow.ScrollableWidth));
|
|
|
|
private void ButtonScrollLeft_Click(object sender, RoutedEventArgs e) =>
|
|
_scrollViewerOverflow.ScrollToHorizontalOffsetInternal(Math.Max(
|
|
_scrollViewerOverflow.CurrentHorizontalOffset - TabItemWidth, 0));
|
|
|
|
private void ButtonOverflow_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_buttonOverflow.IsChecked == true)
|
|
{
|
|
_buttonOverflow.Menu.Items.Clear();
|
|
for (var i = 0; i < Items.Count; i++)
|
|
{
|
|
if(!(ItemContainerGenerator.ContainerFromIndex(i) is TabItem item)) continue;
|
|
|
|
var menuItem = new System.Windows.Controls.MenuItem
|
|
{
|
|
HeaderStringFormat = ItemStringFormat,
|
|
HeaderTemplate = ItemTemplate,
|
|
HeaderTemplateSelector = ItemTemplateSelector,
|
|
Header = item.Header,
|
|
Width = TabItemWidth,
|
|
IsChecked = item.IsSelected,
|
|
IsCheckable = true
|
|
};
|
|
|
|
menuItem.Click += delegate
|
|
{
|
|
_buttonOverflow.IsChecked = false;
|
|
|
|
var list = GetActualList();
|
|
if (list == null) return;
|
|
|
|
var actualItem = ItemContainerGenerator.ItemFromContainer(item);
|
|
if (actualItem == null) return;
|
|
|
|
var index = list.IndexOf(actualItem);
|
|
if (index >= _itemShowCount)
|
|
{
|
|
list.Remove(actualItem);
|
|
list.Insert(0, actualItem);
|
|
if (IsAnimationEnabled)
|
|
{
|
|
HeaderPanel.SetCurrentValue(TabPanel.FluidMoveDurationProperty, new Duration(TimeSpan.FromMilliseconds(200)));
|
|
}
|
|
else
|
|
{
|
|
HeaderPanel.FluidMoveDuration = new Duration(TimeSpan.FromSeconds(0));
|
|
}
|
|
HeaderPanel.ForceUpdate = true;
|
|
HeaderPanel.Measure(new Size(HeaderPanel.DesiredSize.Width, ActualHeight));
|
|
HeaderPanel.ForceUpdate = false;
|
|
SetCurrentValue(SelectedIndexProperty, 0);
|
|
}
|
|
|
|
item.IsSelected = true;
|
|
};
|
|
_buttonOverflow.Menu.Items.Add(menuItem);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal double GetHorizontalOffset() => _scrollViewerOverflow?.CurrentHorizontalOffset ?? 0;
|
|
|
|
internal void UpdateScroll() => _scrollViewerOverflow?.RaiseEvent(new MouseWheelEventArgs(Mouse.PrimaryDevice, Environment.TickCount, 0)
|
|
{
|
|
RoutedEvent = MouseWheelEvent
|
|
});
|
|
|
|
internal void CloseAllItems() => CloseOtherItems(null);
|
|
|
|
internal void CloseOtherItems(TabItem currentItem)
|
|
{
|
|
var actualItem = currentItem != null ? ItemContainerGenerator.ItemFromContainer(currentItem) : null;
|
|
|
|
var list = GetActualList();
|
|
if (list == null) return;
|
|
|
|
IsInternalAction = true;
|
|
|
|
for (var i = 0; i < Items.Count; i++)
|
|
{
|
|
var item = list[i];
|
|
if (!Equals(item, actualItem) && item != null)
|
|
{
|
|
var argsClosing = new CancelRoutedEventArgs(TabItem.ClosingEvent, item);
|
|
|
|
if (!(ItemContainerGenerator.ContainerFromItem(item) is TabItem tabItem)) continue;
|
|
|
|
tabItem.RaiseEvent(argsClosing);
|
|
if (argsClosing.Cancel) return;
|
|
|
|
tabItem.RaiseEvent(new RoutedEventArgs(TabItem.ClosedEvent, item));
|
|
list.Remove(item);
|
|
|
|
i--;
|
|
}
|
|
}
|
|
|
|
SetCurrentValue(SelectedIndexProperty, Items.Count == 0 ? -1 : 0);
|
|
}
|
|
|
|
internal IList GetActualList()
|
|
{
|
|
IList list;
|
|
if (ItemsSource != null)
|
|
{
|
|
list = ItemsSource as IList;
|
|
}
|
|
else
|
|
{
|
|
list = Items;
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
protected override bool IsItemItsOwnContainerOverride(object item) => item is TabItem;
|
|
|
|
protected override DependencyObject GetContainerForItemOverride() => new TabItem();
|
|
}
|
|
} |