TabControl.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Specialized;
  4. using System.Windows;
  5. using System.Windows.Controls;
  6. using System.Windows.Controls.Primitives;
  7. using System.Windows.Data;
  8. using System.Windows.Input;
  9. using HandyControl.Data;
  10. using HandyControl.Tools.Extension;
  11. namespace HandyControl.Controls;
  12. [TemplatePart(Name = OverflowButtonKey, Type = typeof(ContextMenuToggleButton))]
  13. [TemplatePart(Name = HeaderPanelKey, Type = typeof(TabPanel))]
  14. [TemplatePart(Name = OverflowScrollviewer, Type = typeof(ScrollViewer))]
  15. [TemplatePart(Name = ScrollButtonLeft, Type = typeof(ButtonBase))]
  16. [TemplatePart(Name = ScrollButtonRight, Type = typeof(ButtonBase))]
  17. [TemplatePart(Name = HeaderBorder, Type = typeof(Border))]
  18. public class TabControl : System.Windows.Controls.TabControl
  19. {
  20. private const string OverflowButtonKey = "PART_OverflowButton";
  21. private const string HeaderPanelKey = "PART_HeaderPanel";
  22. private const string OverflowScrollviewer = "PART_OverflowScrollviewer";
  23. private const string ScrollButtonLeft = "PART_ScrollButtonLeft";
  24. private const string ScrollButtonRight = "PART_ScrollButtonRight";
  25. private const string HeaderBorder = "PART_HeaderBorder";
  26. private ContextMenuToggleButton _buttonOverflow;
  27. internal TabPanel HeaderPanel { get; private set; }
  28. private ScrollViewer _scrollViewerOverflow;
  29. private ButtonBase _buttonScrollLeft;
  30. private ButtonBase _buttonScrollRight;
  31. private Border _headerBorder;
  32. /// <summary>
  33. /// 是否为内部操作
  34. /// </summary>
  35. internal bool IsInternalAction;
  36. /// <summary>
  37. /// 是否启用动画
  38. /// </summary>
  39. public static readonly DependencyProperty IsAnimationEnabledProperty = DependencyProperty.Register(
  40. nameof(IsAnimationEnabled), typeof(bool), typeof(TabControl), new PropertyMetadata(ValueBoxes.FalseBox));
  41. /// <summary>
  42. /// 是否启用动画
  43. /// </summary>
  44. public bool IsAnimationEnabled
  45. {
  46. get => (bool) GetValue(IsAnimationEnabledProperty);
  47. set => SetValue(IsAnimationEnabledProperty, ValueBoxes.BooleanBox(value));
  48. }
  49. /// <summary>
  50. /// 是否可以拖动
  51. /// </summary>
  52. public static readonly DependencyProperty IsDraggableProperty = DependencyProperty.Register(
  53. nameof(IsDraggable), typeof(bool), typeof(TabControl), new PropertyMetadata(ValueBoxes.FalseBox));
  54. /// <summary>
  55. /// 是否可以拖动
  56. /// </summary>
  57. public bool IsDraggable
  58. {
  59. get => (bool) GetValue(IsDraggableProperty);
  60. set => SetValue(IsDraggableProperty, ValueBoxes.BooleanBox(value));
  61. }
  62. /// <summary>
  63. /// 是否显示关闭按钮
  64. /// </summary>
  65. public static readonly DependencyProperty ShowCloseButtonProperty = DependencyProperty.RegisterAttached(
  66. "ShowCloseButton", typeof(bool), typeof(TabControl), new FrameworkPropertyMetadata(ValueBoxes.FalseBox, FrameworkPropertyMetadataOptions.Inherits));
  67. public static void SetShowCloseButton(DependencyObject element, bool value)
  68. => element.SetValue(ShowCloseButtonProperty, ValueBoxes.BooleanBox(value));
  69. public static bool GetShowCloseButton(DependencyObject element)
  70. => (bool) element.GetValue(ShowCloseButtonProperty);
  71. /// <summary>
  72. /// 是否显示关闭按钮
  73. /// </summary>
  74. public bool ShowCloseButton
  75. {
  76. get => (bool) GetValue(ShowCloseButtonProperty);
  77. set => SetValue(ShowCloseButtonProperty, ValueBoxes.BooleanBox(value));
  78. }
  79. /// <summary>
  80. /// 是否显示上下文菜单
  81. /// </summary>
  82. public static readonly DependencyProperty ShowContextMenuProperty = DependencyProperty.RegisterAttached(
  83. "ShowContextMenu", typeof(bool), typeof(TabControl), new FrameworkPropertyMetadata(ValueBoxes.TrueBox, FrameworkPropertyMetadataOptions.Inherits));
  84. public static void SetShowContextMenu(DependencyObject element, bool value)
  85. => element.SetValue(ShowContextMenuProperty, ValueBoxes.BooleanBox(value));
  86. public static bool GetShowContextMenu(DependencyObject element)
  87. => (bool) element.GetValue(ShowContextMenuProperty);
  88. /// <summary>
  89. /// 是否显示上下文菜单
  90. /// </summary>
  91. public bool ShowContextMenu
  92. {
  93. get => (bool) GetValue(ShowContextMenuProperty);
  94. set => SetValue(ShowContextMenuProperty, ValueBoxes.BooleanBox(value));
  95. }
  96. public static readonly DependencyProperty CanBeClosedByMiddleButtonProperty =
  97. DependencyProperty.Register(nameof(CanBeClosedByMiddleButton), typeof(bool), typeof(TabControl), new PropertyMetadata(ValueBoxes.TrueBox));
  98. public bool CanBeClosedByMiddleButton
  99. {
  100. get => (bool) GetValue(CanBeClosedByMiddleButtonProperty);
  101. set => SetValue(CanBeClosedByMiddleButtonProperty, ValueBoxes.BooleanBox(value));
  102. }
  103. /// <summary>
  104. /// 是否将标签填充
  105. /// </summary>
  106. public static readonly DependencyProperty IsTabFillEnabledProperty = DependencyProperty.Register(
  107. nameof(IsTabFillEnabled), typeof(bool), typeof(TabControl), new PropertyMetadata(ValueBoxes.FalseBox));
  108. /// <summary>
  109. /// 是否将标签填充
  110. /// </summary>
  111. public bool IsTabFillEnabled
  112. {
  113. get => (bool) GetValue(IsTabFillEnabledProperty);
  114. set => SetValue(IsTabFillEnabledProperty, ValueBoxes.BooleanBox(value));
  115. }
  116. /// <summary>
  117. /// 标签宽度
  118. /// </summary>
  119. public static readonly DependencyProperty TabItemWidthProperty = DependencyProperty.Register(
  120. nameof(TabItemWidth), typeof(double), typeof(TabControl), new PropertyMetadata(200.0));
  121. /// <summary>
  122. /// 标签宽度
  123. /// </summary>
  124. public double TabItemWidth
  125. {
  126. get => (double) GetValue(TabItemWidthProperty);
  127. set => SetValue(TabItemWidthProperty, value);
  128. }
  129. /// <summary>
  130. /// 标签高度
  131. /// </summary>
  132. public static readonly DependencyProperty TabItemHeightProperty = DependencyProperty.Register(
  133. nameof(TabItemHeight), typeof(double), typeof(TabControl), new PropertyMetadata(30.0));
  134. /// <summary>
  135. /// 标签高度
  136. /// </summary>
  137. public double TabItemHeight
  138. {
  139. get => (double) GetValue(TabItemHeightProperty);
  140. set => SetValue(TabItemHeightProperty, value);
  141. }
  142. /// <summary>
  143. /// 是否可以滚动
  144. /// </summary>
  145. public static readonly DependencyProperty IsScrollableProperty = DependencyProperty.Register(
  146. nameof(IsScrollable), typeof(bool), typeof(TabControl), new PropertyMetadata(ValueBoxes.FalseBox));
  147. /// <summary>
  148. /// 是否可以滚动
  149. /// </summary>
  150. public bool IsScrollable
  151. {
  152. get => (bool) GetValue(IsScrollableProperty);
  153. set => SetValue(IsScrollableProperty, ValueBoxes.BooleanBox(value));
  154. }
  155. /// <summary>
  156. /// 是否显示溢出按钮
  157. /// </summary>
  158. public static readonly DependencyProperty ShowOverflowButtonProperty = DependencyProperty.Register(
  159. nameof(ShowOverflowButton), typeof(bool), typeof(TabControl), new PropertyMetadata(ValueBoxes.TrueBox));
  160. /// <summary>
  161. /// 是否显示溢出按钮
  162. /// </summary>
  163. public bool ShowOverflowButton
  164. {
  165. get => (bool) GetValue(ShowOverflowButtonProperty);
  166. set => SetValue(ShowOverflowButtonProperty, ValueBoxes.BooleanBox(value));
  167. }
  168. /// <summary>
  169. /// 是否显示滚动按钮
  170. /// </summary>
  171. public static readonly DependencyProperty ShowScrollButtonProperty = DependencyProperty.Register(
  172. nameof(ShowScrollButton), typeof(bool), typeof(TabControl), new PropertyMetadata(ValueBoxes.FalseBox));
  173. /// <summary>
  174. /// 是否显示滚动按钮
  175. /// </summary>
  176. public bool ShowScrollButton
  177. {
  178. get => (bool) GetValue(ShowScrollButtonProperty);
  179. set => SetValue(ShowScrollButtonProperty, ValueBoxes.BooleanBox(value));
  180. }
  181. public static readonly DependencyProperty OverflowMenuDisplayMemberPathProperty = DependencyProperty.Register(
  182. nameof(OverflowMenuDisplayMemberPath), typeof(string), typeof(TabControl), new PropertyMetadata(default(string)));
  183. public string OverflowMenuDisplayMemberPath
  184. {
  185. get => (string) GetValue(OverflowMenuDisplayMemberPathProperty);
  186. set => SetValue(OverflowMenuDisplayMemberPathProperty, value);
  187. }
  188. /// <summary>
  189. /// 可见的标签数量
  190. /// </summary>
  191. private int _itemShowCount;
  192. protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
  193. {
  194. base.OnItemsChanged(e);
  195. if (HeaderPanel == null)
  196. {
  197. IsInternalAction = false;
  198. return;
  199. }
  200. UpdateOverflowButton();
  201. if (IsInternalAction)
  202. {
  203. IsInternalAction = false;
  204. return;
  205. }
  206. if (e.Action == NotifyCollectionChangedAction.Add)
  207. {
  208. for (var i = 0; i < Items.Count; i++)
  209. {
  210. if (ItemContainerGenerator.ContainerFromIndex(i) is not TabItem item) return;
  211. item.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
  212. item.TabPanel = HeaderPanel;
  213. }
  214. }
  215. _headerBorder?.InvalidateMeasure();
  216. IsInternalAction = false;
  217. }
  218. public override void OnApplyTemplate()
  219. {
  220. if (_buttonOverflow != null)
  221. {
  222. if (_buttonOverflow.Menu != null)
  223. {
  224. _buttonOverflow.Menu.Closed -= Menu_Closed;
  225. _buttonOverflow.Menu = null;
  226. }
  227. _buttonOverflow.Click -= ButtonOverflow_Click;
  228. }
  229. if (_buttonScrollLeft != null) _buttonScrollLeft.Click -= ButtonScrollLeft_Click;
  230. if (_buttonScrollRight != null) _buttonScrollRight.Click -= ButtonScrollRight_Click;
  231. base.OnApplyTemplate();
  232. HeaderPanel = GetTemplateChild(HeaderPanelKey) as TabPanel;
  233. if (IsTabFillEnabled) return;
  234. _buttonOverflow = GetTemplateChild(OverflowButtonKey) as ContextMenuToggleButton;
  235. _scrollViewerOverflow = GetTemplateChild(OverflowScrollviewer) as ScrollViewer;
  236. _buttonScrollLeft = GetTemplateChild(ScrollButtonLeft) as ButtonBase;
  237. _buttonScrollRight = GetTemplateChild(ScrollButtonRight) as ButtonBase;
  238. _headerBorder = GetTemplateChild(HeaderBorder) as Border;
  239. if (_buttonScrollLeft != null) _buttonScrollLeft.Click += ButtonScrollLeft_Click;
  240. if (_buttonScrollRight != null) _buttonScrollRight.Click += ButtonScrollRight_Click;
  241. if (_buttonOverflow != null)
  242. {
  243. var menu = new ContextMenu
  244. {
  245. Placement = PlacementMode.Bottom,
  246. PlacementTarget = _buttonOverflow
  247. };
  248. menu.Closed += Menu_Closed;
  249. _buttonOverflow.Menu = menu;
  250. _buttonOverflow.Click += ButtonOverflow_Click;
  251. }
  252. }
  253. protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
  254. {
  255. base.OnRenderSizeChanged(sizeInfo);
  256. UpdateOverflowButton();
  257. }
  258. private void UpdateOverflowButton()
  259. {
  260. if (!IsTabFillEnabled)
  261. {
  262. double actualWidth = ActualWidth;
  263. double tabItemWidth = TabItemWidth;
  264. _itemShowCount = (int) (actualWidth / tabItemWidth);
  265. _buttonOverflow?.Show(Items.Count > 0 && Items.Count * tabItemWidth >= actualWidth && ShowOverflowButton);
  266. }
  267. }
  268. private void Menu_Closed(object sender, RoutedEventArgs e) => _buttonOverflow.IsChecked = false;
  269. private void ButtonScrollRight_Click(object sender, RoutedEventArgs e) =>
  270. _scrollViewerOverflow.ScrollToHorizontalOffsetWithAnimation(Math.Min(
  271. _scrollViewerOverflow.CurrentHorizontalOffset + TabItemWidth, _scrollViewerOverflow.ScrollableWidth));
  272. private void ButtonScrollLeft_Click(object sender, RoutedEventArgs e) =>
  273. _scrollViewerOverflow.ScrollToHorizontalOffsetWithAnimation(Math.Max(
  274. _scrollViewerOverflow.CurrentHorizontalOffset - TabItemWidth, 0));
  275. private void ButtonOverflow_Click(object sender, RoutedEventArgs e)
  276. {
  277. if (_buttonOverflow.IsChecked == true)
  278. {
  279. _buttonOverflow.Menu.Items.Clear();
  280. for (var i = 0; i < Items.Count; i++)
  281. {
  282. if (ItemContainerGenerator.ContainerFromIndex(i) is not TabItem item) continue;
  283. var menuItem = new MenuItem
  284. {
  285. HeaderStringFormat = ItemStringFormat,
  286. IsChecked = item.IsSelected,
  287. IsCheckable = true,
  288. IsEnabled = item.IsEnabled
  289. };
  290. if (item.DataContext is not null)
  291. {
  292. if (ItemTemplate is null)
  293. {
  294. menuItem.SetBinding(HeaderedItemsControl.HeaderProperty, new Binding(DisplayMemberPath)
  295. {
  296. Source = item.DataContext
  297. });
  298. }
  299. else
  300. {
  301. menuItem.SetBinding(HeaderedItemsControl.HeaderProperty, new Binding(OverflowMenuDisplayMemberPath)
  302. {
  303. Source = item.DataContext
  304. });
  305. }
  306. }
  307. else
  308. {
  309. menuItem.SetBinding(HeaderedItemsControl.HeaderProperty, new Binding(HeaderedItemsControl.HeaderProperty.Name)
  310. {
  311. Source = item
  312. });
  313. }
  314. menuItem.Click += delegate
  315. {
  316. _buttonOverflow.IsChecked = false;
  317. var list = GetActualList();
  318. if (list == null) return;
  319. var actualItem = ItemContainerGenerator.ItemFromContainer(item);
  320. if (actualItem == null) return;
  321. var index = list.IndexOf(actualItem);
  322. if (index >= _itemShowCount)
  323. {
  324. list.Remove(actualItem);
  325. list.Insert(0, actualItem);
  326. HeaderPanel.SetValue(TabPanel.FluidMoveDurationPropertyKey,
  327. IsAnimationEnabled
  328. ? new Duration(TimeSpan.FromMilliseconds(200))
  329. : new Duration(TimeSpan.FromMilliseconds(0)));
  330. HeaderPanel.ForceUpdate = true;
  331. HeaderPanel.Measure(new Size(HeaderPanel.DesiredSize.Width, ActualHeight));
  332. HeaderPanel.ForceUpdate = false;
  333. SetCurrentValue(SelectedIndexProperty, ValueBoxes.Int0Box);
  334. }
  335. item.IsSelected = true;
  336. };
  337. _buttonOverflow.Menu.Items.Add(menuItem);
  338. }
  339. }
  340. }
  341. internal double GetHorizontalOffset() => _scrollViewerOverflow?.CurrentHorizontalOffset ?? 0;
  342. internal void UpdateScroll() => _scrollViewerOverflow?.RaiseEvent(new MouseWheelEventArgs(Mouse.PrimaryDevice, Environment.TickCount, 0)
  343. {
  344. RoutedEvent = MouseWheelEvent
  345. });
  346. internal void CloseAllItems() => CloseOtherItems(null);
  347. internal void CloseOtherItems(TabItem currentItem)
  348. {
  349. var actualItem = currentItem != null ? ItemContainerGenerator.ItemFromContainer(currentItem) : null;
  350. var list = GetActualList();
  351. if (list == null) return;
  352. IsInternalAction = true;
  353. for (var i = 0; i < Items.Count; i++)
  354. {
  355. var item = list[i];
  356. if (!Equals(item, actualItem) && item != null)
  357. {
  358. var argsClosing = new CancelRoutedEventArgs(TabItem.ClosingEvent, item);
  359. if (ItemContainerGenerator.ContainerFromItem(item) is not TabItem tabItem) continue;
  360. tabItem.RaiseEvent(argsClosing);
  361. if (argsClosing.Cancel)
  362. {
  363. continue;
  364. }
  365. tabItem.RaiseEvent(new RoutedEventArgs(TabItem.ClosedEvent, item));
  366. list.Remove(item);
  367. i--;
  368. }
  369. }
  370. SetCurrentValue(SelectedIndexProperty, Items.Count == 0 ? -1 : 0);
  371. }
  372. internal IList GetActualList()
  373. {
  374. IList list;
  375. if (ItemsSource != null)
  376. {
  377. list = ItemsSource as IList;
  378. }
  379. else
  380. {
  381. list = Items;
  382. }
  383. return list;
  384. }
  385. protected override bool IsItemItsOwnContainerOverride(object item) => item is TabItem;
  386. protected override DependencyObject GetContainerForItemOverride() => new TabItem();
  387. }