TabItem.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. using System;
  2. using System.Windows;
  3. using System.Windows.Controls;
  4. using System.Windows.Controls.Primitives;
  5. using System.Windows.Data;
  6. using System.Windows.Input;
  7. using System.Windows.Media;
  8. using System.Windows.Media.Animation;
  9. using HandyControl.Data;
  10. using HandyControl.Interactivity;
  11. using HandyControl.Tools;
  12. using HandyControl.Tools.Extension;
  13. namespace HandyControl.Controls;
  14. public class TabItem : System.Windows.Controls.TabItem
  15. {
  16. /// <summary>
  17. /// 动画速度
  18. /// </summary>
  19. private const int AnimationSpeed = 150;
  20. /// <summary>
  21. /// 选项卡是否处于拖动状态
  22. /// </summary>
  23. private static bool ItemIsDragging;
  24. /// <summary>
  25. /// 选项卡是否等待被拖动
  26. /// </summary>
  27. private bool _isWaiting;
  28. /// <summary>
  29. /// 拖动中的选项卡坐标
  30. /// </summary>
  31. private Point _dragPoint;
  32. /// <summary>
  33. /// 鼠标按下时选项卡位置
  34. /// </summary>
  35. private int _mouseDownIndex;
  36. /// <summary>
  37. /// 鼠标按下时选项卡横向偏移
  38. /// </summary>
  39. private double _mouseDownOffsetX;
  40. /// <summary>
  41. /// 鼠标按下时的坐标
  42. /// </summary>
  43. private Point _mouseDownPoint;
  44. /// <summary>
  45. /// 右侧可移动的最大值
  46. /// </summary>
  47. private double _maxMoveRight;
  48. /// <summary>
  49. /// 左侧可移动的最大值
  50. /// </summary>
  51. private double _maxMoveLeft;
  52. /// <summary>
  53. /// 选项卡宽度
  54. /// </summary>
  55. public double ItemWidth { get; internal set; }
  56. /// <summary>
  57. /// 选项卡拖动等待距离(在鼠标移动了超过20个像素无关单位后,选项卡才开始被拖动)
  58. /// </summary>
  59. private const double WaitLength = 20;
  60. /// <summary>
  61. /// 选项卡是否处于拖动状态
  62. /// </summary>
  63. private bool _isDragging;
  64. /// <summary>
  65. /// 选项卡是否已经被拖动
  66. /// </summary>
  67. private bool _isDragged;
  68. /// <summary>
  69. /// 目标横向位移
  70. /// </summary>
  71. internal double TargetOffsetX { get; set; }
  72. /// <summary>
  73. /// 当前编号
  74. /// </summary>
  75. private int _currentIndex;
  76. /// <summary>
  77. /// 标签容器横向滚动距离
  78. /// </summary>
  79. private double _scrollHorizontalOffset;
  80. private TabPanel _tabPanel;
  81. /// <summary>
  82. /// 标签容器
  83. /// </summary>
  84. internal TabPanel TabPanel
  85. {
  86. get
  87. {
  88. if (_tabPanel == null && TabControlParent != null)
  89. {
  90. _tabPanel = TabControlParent.HeaderPanel;
  91. }
  92. return _tabPanel;
  93. }
  94. set => _tabPanel = value;
  95. }
  96. /// <summary>
  97. /// 当前编号
  98. /// </summary>
  99. internal int CurrentIndex
  100. {
  101. get => _currentIndex;
  102. set
  103. {
  104. if (_currentIndex == value || value < 0) return;
  105. var oldIndex = _currentIndex;
  106. _currentIndex = value;
  107. UpdateItemOffsetX(oldIndex);
  108. }
  109. }
  110. /// <summary>
  111. /// 是否显示关闭按钮
  112. /// </summary>
  113. public static readonly DependencyProperty ShowCloseButtonProperty =
  114. TabControl.ShowCloseButtonProperty.AddOwner(typeof(TabItem));
  115. /// <summary>
  116. /// 是否显示关闭按钮
  117. /// </summary>
  118. public bool ShowCloseButton
  119. {
  120. get => (bool) GetValue(ShowCloseButtonProperty);
  121. set => SetValue(ShowCloseButtonProperty, ValueBoxes.BooleanBox(value));
  122. }
  123. public static void SetShowCloseButton(DependencyObject element, bool value)
  124. => element.SetValue(ShowCloseButtonProperty, ValueBoxes.BooleanBox(value));
  125. public static bool GetShowCloseButton(DependencyObject element)
  126. => (bool) element.GetValue(ShowCloseButtonProperty);
  127. /// <summary>
  128. /// 是否显示上下文菜单
  129. /// </summary>
  130. public static readonly DependencyProperty ShowContextMenuProperty =
  131. TabControl.ShowContextMenuProperty.AddOwner(typeof(TabItem), new FrameworkPropertyMetadata(OnShowContextMenuChanged));
  132. private static void OnShowContextMenuChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  133. {
  134. var ctl = (TabItem) d;
  135. if (ctl.Menu != null)
  136. {
  137. var show = (bool) e.NewValue;
  138. ctl.Menu.IsEnabled = show;
  139. ctl.Menu.Show(show);
  140. }
  141. }
  142. /// <summary>
  143. /// 是否显示上下文菜单
  144. /// </summary>
  145. public bool ShowContextMenu
  146. {
  147. get => (bool) GetValue(ShowContextMenuProperty);
  148. set => SetValue(ShowContextMenuProperty, ValueBoxes.BooleanBox(value));
  149. }
  150. public static void SetShowContextMenu(DependencyObject element, bool value)
  151. => element.SetValue(ShowContextMenuProperty, ValueBoxes.BooleanBox(value));
  152. public static bool GetShowContextMenu(DependencyObject element)
  153. => (bool) element.GetValue(ShowContextMenuProperty);
  154. public static readonly DependencyProperty MenuProperty = DependencyProperty.Register(
  155. nameof(Menu), typeof(ContextMenu), typeof(TabItem), new PropertyMetadata(default(ContextMenu), OnMenuChanged));
  156. private static void OnMenuChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  157. {
  158. var ctl = (TabItem) d;
  159. ctl.OnMenuChanged(e.NewValue as ContextMenu);
  160. }
  161. private void OnMenuChanged(ContextMenu menu)
  162. {
  163. if (IsLoaded && menu != null)
  164. {
  165. var parent = TabControlParent;
  166. if (parent == null) return;
  167. var item = parent.ItemContainerGenerator.ItemFromContainer(this);
  168. menu.DataContext = item;
  169. menu.SetBinding(IsEnabledProperty, new Binding(ShowContextMenuProperty.Name)
  170. {
  171. Source = this
  172. });
  173. menu.SetBinding(VisibilityProperty, new Binding(ShowContextMenuProperty.Name)
  174. {
  175. Source = this,
  176. Converter = ResourceHelper.GetResourceInternal<IValueConverter>(ResourceToken.Boolean2VisibilityConverter)
  177. });
  178. }
  179. }
  180. public ContextMenu Menu
  181. {
  182. get => (ContextMenu) GetValue(MenuProperty);
  183. set => SetValue(MenuProperty, value);
  184. }
  185. /// <summary>
  186. /// 更新选项卡横向偏移
  187. /// </summary>
  188. /// <param name="oldIndex"></param>
  189. private void UpdateItemOffsetX(int oldIndex)
  190. {
  191. if (!_isDragging || CurrentIndex >= TabPanel.ItemDic.Count)
  192. {
  193. return;
  194. }
  195. var moveItem = TabPanel.ItemDic[CurrentIndex];
  196. moveItem.CurrentIndex -= CurrentIndex - oldIndex;
  197. var offsetX = moveItem.TargetOffsetX;
  198. var resultX = offsetX + (oldIndex - CurrentIndex) * ItemWidth;
  199. TabPanel.ItemDic[CurrentIndex] = this;
  200. TabPanel.ItemDic[moveItem.CurrentIndex] = moveItem;
  201. moveItem.CreateAnimation(offsetX, resultX);
  202. }
  203. public TabItem()
  204. {
  205. CommandBindings.Add(new CommandBinding(ControlCommands.Close, (s, e) => Close()));
  206. CommandBindings.Add(new CommandBinding(ControlCommands.CloseAll, (s, e) => TabControlParent.CloseAllItems()));
  207. CommandBindings.Add(new CommandBinding(
  208. ControlCommands.CloseOther,
  209. (s, e) => TabControlParent.CloseOtherItems(this),
  210. (s, e) => e.CanExecute = TabControlParent.Items.Count > 1));
  211. Loaded += (s, e) => OnMenuChanged(Menu);
  212. }
  213. private TabControl TabControlParent => ItemsControl.ItemsControlFromItemContainer(this) as TabControl;
  214. protected override void OnMouseRightButtonDown(MouseButtonEventArgs e)
  215. {
  216. base.OnMouseRightButtonDown(e);
  217. if (!IsMouseOverHeader(e))
  218. {
  219. return;
  220. }
  221. IsSelected = true;
  222. Focus();
  223. }
  224. protected override void OnHeaderChanged(object oldHeader, object newHeader)
  225. {
  226. base.OnHeaderChanged(oldHeader, newHeader);
  227. if (TabPanel != null)
  228. {
  229. TabPanel.ForceUpdate = true;
  230. InvalidateMeasure();
  231. TabPanel.ForceUpdate = true;
  232. }
  233. }
  234. internal void Close()
  235. {
  236. var parent = TabControlParent;
  237. if (parent == null) return;
  238. var item = parent.ItemContainerGenerator.ItemFromContainer(this);
  239. var argsClosing = new CancelRoutedEventArgs(ClosingEvent, item);
  240. RaiseEvent(argsClosing);
  241. if (argsClosing.Cancel) return;
  242. TabPanel.SetValue(TabPanel.FluidMoveDurationPropertyKey, parent.IsAnimationEnabled
  243. ? new Duration(TimeSpan.FromMilliseconds(200))
  244. : new Duration(TimeSpan.FromMilliseconds(1)));
  245. parent.IsInternalAction = true;
  246. RaiseEvent(new RoutedEventArgs(ClosedEvent, item));
  247. var list = parent.GetActualList();
  248. list?.Remove(item);
  249. }
  250. protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
  251. {
  252. base.OnMouseLeftButtonDown(e);
  253. if (!IsMouseOverHeader(e))
  254. {
  255. return;
  256. }
  257. var parent = TabControlParent;
  258. if (parent == null) return;
  259. if (parent.IsDraggable && !ItemIsDragging && !_isDragging)
  260. {
  261. parent.UpdateScroll();
  262. TabPanel.SetValue(TabPanel.FluidMoveDurationPropertyKey, new Duration(TimeSpan.FromSeconds(0)));
  263. _mouseDownOffsetX = RenderTransform.Value.OffsetX;
  264. _scrollHorizontalOffset = parent.GetHorizontalOffset();
  265. var mx = TranslatePoint(new Point(), parent).X + _scrollHorizontalOffset;
  266. _mouseDownIndex = CalLocationIndex(mx);
  267. var subIndex = _mouseDownIndex - CalLocationIndex(_scrollHorizontalOffset);
  268. _maxMoveLeft = -subIndex * ItemWidth;
  269. _maxMoveRight = parent.ActualWidth - ActualWidth + _maxMoveLeft;
  270. _isDragging = true;
  271. ItemIsDragging = true;
  272. _isWaiting = true;
  273. _dragPoint = e.GetPosition(parent);
  274. _dragPoint = new Point(_dragPoint.X + _scrollHorizontalOffset, _dragPoint.Y);
  275. _mouseDownPoint = _dragPoint;
  276. CaptureMouse();
  277. }
  278. }
  279. protected override void OnMouseMove(MouseEventArgs e)
  280. {
  281. base.OnMouseMove(e);
  282. if (ItemIsDragging && _isDragging)
  283. {
  284. var parent = TabControlParent;
  285. if (parent == null) return;
  286. var subX = TranslatePoint(new Point(), parent).X + _scrollHorizontalOffset;
  287. CurrentIndex = CalLocationIndex(subX);
  288. var p = e.GetPosition(parent);
  289. p = new Point(p.X + _scrollHorizontalOffset, p.Y);
  290. var subLeft = p.X - _dragPoint.X;
  291. var totalLeft = p.X - _mouseDownPoint.X;
  292. if (Math.Abs(subLeft) <= WaitLength && _isWaiting) return;
  293. _isWaiting = false;
  294. _isDragged = true;
  295. var left = subLeft + RenderTransform.Value.OffsetX;
  296. if (totalLeft < _maxMoveLeft)
  297. {
  298. left = _maxMoveLeft + _mouseDownOffsetX;
  299. }
  300. else if (totalLeft > _maxMoveRight)
  301. {
  302. left = _maxMoveRight + _mouseDownOffsetX;
  303. }
  304. var t = new TranslateTransform(left, 0);
  305. RenderTransform = t;
  306. _dragPoint = p;
  307. }
  308. }
  309. protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
  310. {
  311. base.OnMouseLeftButtonUp(e);
  312. ReleaseMouseCapture();
  313. if (_isDragged)
  314. {
  315. var parent = TabControlParent;
  316. if (parent == null) return;
  317. var subX = TranslatePoint(new Point(), parent).X + _scrollHorizontalOffset;
  318. var index = CalLocationIndex(subX);
  319. var left = index * ItemWidth;
  320. var offsetX = RenderTransform.Value.OffsetX;
  321. CreateAnimation(offsetX, offsetX - subX + left, index);
  322. }
  323. _isDragging = false;
  324. ItemIsDragging = false;
  325. _isDragged = false;
  326. }
  327. protected override void OnMouseDown(MouseButtonEventArgs e)
  328. {
  329. if (e is { ChangedButton: MouseButton.Middle, ButtonState: MouseButtonState.Pressed } &&
  330. TabControlParent.CanBeClosedByMiddleButton &&
  331. IsMouseOverHeader(e))
  332. {
  333. if (ShowCloseButton || ShowContextMenu)
  334. {
  335. Close();
  336. }
  337. }
  338. }
  339. /// <summary>
  340. /// 创建动画
  341. /// </summary>
  342. internal void CreateAnimation(double offsetX, double resultX, int index = -1)
  343. {
  344. var parent = TabControlParent;
  345. void AnimationCompleted()
  346. {
  347. RenderTransform = new TranslateTransform(resultX, 0);
  348. if (index == -1) return;
  349. var list = parent.GetActualList();
  350. if (list == null) return;
  351. var item = parent.ItemContainerGenerator.ItemFromContainer(this);
  352. if (item == null) return;
  353. TabPanel.CanUpdate = false;
  354. parent.IsInternalAction = true;
  355. list.Remove(item);
  356. parent.IsInternalAction = true;
  357. list.Insert(index, item);
  358. _tabPanel.SetValue(TabPanel.FluidMoveDurationPropertyKey, new Duration(TimeSpan.FromMilliseconds(0)));
  359. TabPanel.CanUpdate = true;
  360. TabPanel.ForceUpdate = true;
  361. TabPanel.Measure(new Size(TabPanel.DesiredSize.Width, ActualHeight));
  362. TabPanel.ForceUpdate = false;
  363. Focus();
  364. IsSelected = true;
  365. if (!IsMouseCaptured)
  366. {
  367. parent.SetCurrentValue(Selector.SelectedIndexProperty, _currentIndex);
  368. }
  369. }
  370. TargetOffsetX = resultX;
  371. if (!parent.IsAnimationEnabled)
  372. {
  373. AnimationCompleted();
  374. return;
  375. }
  376. var animation = AnimationHelper.CreateAnimation(resultX, AnimationSpeed);
  377. animation.FillBehavior = FillBehavior.Stop;
  378. animation.Completed += (s1, e1) => AnimationCompleted();
  379. var f = new TranslateTransform(offsetX, 0);
  380. RenderTransform = f;
  381. f.BeginAnimation(TranslateTransform.XProperty, animation, HandoffBehavior.Compose);
  382. }
  383. /// <summary>
  384. /// 计算选项卡当前合适的位置编号
  385. /// </summary>
  386. /// <param name="left"></param>
  387. /// <returns></returns>
  388. private int CalLocationIndex(double left)
  389. {
  390. if (_isWaiting)
  391. {
  392. return CurrentIndex;
  393. }
  394. var maxIndex = TabControlParent.Items.Count - 1;
  395. var div = (int) (left / ItemWidth);
  396. var rest = left % ItemWidth;
  397. var result = rest / ItemWidth > .5 ? div + 1 : div;
  398. return result > maxIndex ? maxIndex : result;
  399. }
  400. private bool IsMouseOverHeader(MouseButtonEventArgs e)
  401. {
  402. return VisualTreeHelper.HitTest(this, e.GetPosition(this)) is not null;
  403. }
  404. public static readonly RoutedEvent ClosingEvent = EventManager.RegisterRoutedEvent("Closing", RoutingStrategy.Bubble, typeof(EventHandler), typeof(TabItem));
  405. public event EventHandler Closing
  406. {
  407. add => AddHandler(ClosingEvent, value);
  408. remove => RemoveHandler(ClosingEvent, value);
  409. }
  410. public static readonly RoutedEvent ClosedEvent = EventManager.RegisterRoutedEvent("Closed", RoutingStrategy.Bubble, typeof(EventHandler), typeof(TabItem));
  411. public event EventHandler Closed
  412. {
  413. add => AddHandler(ClosedEvent, value);
  414. remove => RemoveHandler(ClosedEvent, value);
  415. }
  416. }