WPF 控件库——可拖动选项卡的TabControl

WPF 控件库系列博文地址:html

WPF 控件库——仿制Chrome的ColorPickergit

WPF 控件库——仿制Windows10的进度条github

WPF 控件库——轮播控件ide

WPF 控件库——带有惯性的ScrollViewer动画

WPF 控件库——可拖动选项卡的TabControlui

 

1、先看看效果spa

 

2、原理rest

一、选项卡大小和位置code

  此次给你们介绍的控件是比较经常使用的TabControl,网上常见的TabControl样式有不少,其中一部分也支持拖动选项卡,可是带动画效果的不多见。这也是有缘由的,由于想要作一个不失原有功能,还须要添加动画效果的控件可不是一行代码的事。要作成上图中的效果,咱们不能一蹴而就,最忌讳的是一上来就想实现全部效果。htm

  一开始,咱们最好先用Blend看看原生的TabControl样式模板部分是如何实现的,这样咱们也好有个参考。咱们先从资产面板中拖一个TabControl放到窗体中,调整好合适的大小:

  而后在它上面右键,编辑模板->编辑副本->肯定,在自动生成的xaml代码中关键部分是这里:

  能够看到,全部的选项卡(也就是TabItem)其实都是放在TabControl内部维护的一个TabPanel中,知道这些就够了,咱们彻底能够作一个定制的TabPanel来替换它: public class TabPanel : Panel 。既然这个TabPanel是一个容器,因此它必须负责计算TabItem的大小还要安排它的位置,咱们能够重载父类Panel的 MeasureOverride 方法来处理这些逻辑: protected override Size MeasureOverride(Size constraint) 。在这个方法中咱们经过 InternalChildren 这个只读属性来获取选项卡,选项卡的高度咱们由 TabItemHeight 属性指定,因为TabPanel对用户是透明的,因此咱们还要定制一个TabControl,里面加上 TabItemHeight 属性,让它和TabPanel的绑定。以后的 TabItemWidth 和 IsEnableTabFill 也同理。而选项卡的宽度则要分状况讨论了,若是 IsEnableTabFill = true 咱们则要平分宽度,例如容器宽度为100,选项卡有10个,那么每一个选项卡的宽度就是10。在这里要注意的是,选项卡的宽度最好不要有小数点,虽然有诸如 UseLayoutRounding 这种特性的帮助能够必定程度去除模糊,但在一个个连续排列的选项卡上反而会拔苗助长,你会发现两两之间的分割线宽度是不一致的,最好的办法就是“不公平的平分”,贴上一段代码来解释:

public static int[] DivideInt2Arr(int num, int count)
{
  var arr = new int[count];
  var div = num / count;
  var rest = num % count;
  for (int i = 0; i < count; i++)
  {
    arr[i] = div;
  }
  for (int i = 0; i < rest; i++)
  {
    arr[i] += 1;
  }
  return arr;
}

  假设如今的容器宽度是108,选项卡仍是10个,经过 MeasureOverride 方法处理后,前八个的宽度则是11,后两个是10。若是 IsEnableTabFill = false 则不要平分了,直接放入容器便可。

  如今选项卡大小搞定了,位置呢?太简单了,一个for循环不断叠加每一个选项卡的宽度就能够了: size.Width += tabItem.ItemWidth; 。最后经过调用 Element.Arrange 便可排布选项卡的位置:

var rect = new Rect
{
    X = size.Width - tabItem.BorderThickness.Left,
    Width = itemWidth,
    Height = TabItemHeight
};
tabItem.Arrange(rect);

  由于选项卡左右都有边距,减去一个左边距,二者间的间隔就是一个边距了。

  选项卡大小和位置的逻辑处理大体是上述的过程,因为篇幅有限,加之我不喜欢一贴一大段代码,因此只挑重点来讨论,完整的代码还要考虑各类状况,这里就再也不赘述了。

 

二、动画处理

  这一部分咱们的关注点就是鼠标了,对选项卡而言,鼠标按下、鼠标移动、鼠标抬起,这些咱们都要关注,因此分别给它们订阅一下事件。与之对应的,咱们还要给选项卡添加几个标私有字段,用以记录状态,好比 _isDragging 、 _isDragged 、 _dragPoint 、 _isWaiting ,前两个我就不说了,都是字面意思,第三个则用来暂存鼠标移动时的位置,每次进入选项卡的 OnMouseMove 事件,都要将 _isDragged 和其旧值做差,以求得当前选项卡应该移动的距离。 _isWaiting 用途比较特殊,在用户拖动选项卡时,咱们最好等待一个粘滞距离,好比20个单位宽度,也就是说,在水平方向鼠标移动了超过20个像素无关单位后,选项卡才开始被拖动。

  在一开始的gif中能够看到,被拖动的选项卡改变位置时,其他的选项卡也会动态改变位置,那么位置改变的时机是如何肯定的呢?很简单,只要将被拖动的选项卡到容器(TabPanel)左边界的这个距离除以 ItemWidth ,结果四舍五入就是这个选项卡当前应该所处的位置,紧接着下一步就是要把这个位置上的选项卡和当前被拖动的换个位置。此刻咱们终于能够用动画来实现了,因为这个系列的文章屡次讲过动画的代码了,因此就再也不赘述。

  上面一段讲的是换位置,那么添加选项卡、删除选项卡呢?其实有个捷径能够走,就是使用 FluidMoveBehavior ,把他往样式里一塞,好了,效果出来了!

  可是这里有一个坑要注意, FluidMoveBehavior 虽然能够化简一部分动画逻辑,可是它有点越权了,它把你位置移动的逻辑也给作了,你会发现,若是不加处理,在你本身的动画结束后它还会再来一遍它的动画。能够将 FluidMoveBehavior 的 Duration 属性暂时归零来解决这个问题: FluidMoveDuration = new Duration(TimeSpan.FromSeconds(0)); 。

  这篇文章只是大体介绍一下实现的过程和思路,感兴趣的能够下载源码,多多交流,共同提升。

 

3、源码

  本文所讨论的控件源码已经在github开源:https://github.com/NaBian/HandyControl