WPF 控件库——可拖动选项卡的TabControl
WPF 控件库系列博文地址:
一、先看看效果

二、原理
1、选项卡大小和位置
这次给大家介绍的控件是比较常用的TabControl,网上常见的TabControl样式有很多,其中一部分也支持拖动选项卡,但是带动画效果的很少见。这也是有原因的,因为想要做一个不失原有功能,还需要添加动画效果的控件可不是一行代码的事。要做成上图中的效果,我们不能一蹴而就,最忌讳的是一上来就想实现所有效果。
一开始,我们最好先用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 = ; i < count; i++)
{
arr[i] = div;
}
for (int i = ; i < rest; i++)
{
arr[i] += ;
}
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);
因为选项卡左右都有边距,减去一个左边距,两者间的间隔就是一个边距了。
选项卡大小和位置的逻辑处理大致是上述的过程,由于篇幅有限,加之我不喜欢一贴一大段代码,所以只挑重点来讨论,完整的代码还要考虑各种情况,这里就不再赘述了。
2、动画处理
这一部分我们的关注点就是鼠标了,对选项卡而言,鼠标按下、鼠标移动、鼠标抬起,这些我们都要关注,所以分别给它们订阅一下事件。与之对应的,我们还要给选项卡添加几个标私有字段,用以记录状态,比如 _isDragging 、 _isDragged 、 _dragPoint 、 _isWaiting ,前两个我就不说了,都是字面意思,第三个则用来暂存鼠标移动时的位置,每次进入选项卡的 OnMouseMove 事件,都要将 _isDragged 和其旧值作差,以求得当前选项卡应该移动的距离。 _isWaiting 用途比较特殊,在用户拖动选项卡时,我们最好等待一个粘滞距离,比如20个单位宽度,也就是说,在水平方向鼠标移动了超过20个像素无关单位后,选项卡才开始被拖动。
在一开始的gif中可以看到,被拖动的选项卡改变位置时,其余的选项卡也会动态改变位置,那么位置改变的时机是如何确定的呢?很简单,只要将被拖动的选项卡到容器(TabPanel)左边界的这个距离除以 ItemWidth ,结果四舍五入就是这个选项卡当前应该所处的位置,紧接着下一步就是要把这个位置上的选项卡和当前被拖动的换个位置。此刻我们终于可以用动画来实现了,由于这个系列的文章多次讲过动画的代码了,所以就不再赘述。
上面一段讲的是换位置,那么添加选项卡、删除选项卡呢?其实有个捷径可以走,就是使用 FluidMoveBehavior ,把他往样式里一塞,好了,效果出来了!

但是这里有一个坑要注意, FluidMoveBehavior 虽然可以化简一部分动画逻辑,但是它有点越权了,它把你位置移动的逻辑也给做了,你会发现,如果不加处理,在你自己的动画结束后它还会再来一遍它的动画。可以将 FluidMoveBehavior 的 Duration 属性暂时归零来解决这个问题: FluidMoveDuration = new Duration(TimeSpan.FromSeconds()); 。
这篇文章只是大致介绍一下实现的过程和思路,感兴趣的可以下载源码,多多交流,共同提高。
三、源码
本文所讨论的控件源码已经在github开源:https://github.com/NaBian/HandyControl
WPF 控件库——可拖动选项卡的TabControl的更多相关文章
- 反爬虫:利用ASP.NET MVC的Filter和缓存(入坑出坑) C#中缓存的使用 C#操作redis WPF 控件库——可拖动选项卡的TabControl 【Bootstrap系列】详解Bootstrap-table AutoFac event 和delegate的分别 常见的异步方式async 和 await C# Task用法 c#源码的执行过程
反爬虫:利用ASP.NET MVC的Filter和缓存(入坑出坑) 背景介绍: 为了平衡社区成员的贡献和索取,一起帮引入了帮帮币.当用户积分(帮帮点)达到一定数额之后,就会“掉落”一定数量的“帮帮 ...
- WPF 控件库——仿制Windows10的进度条
WPF 控件库系列博文地址: WPF 控件库——仿制Chrome的ColorPicker WPF 控件库——仿制Windows10的进度条 WPF 控件库——轮播控件 WPF 控件库——带有惯性的Sc ...
- WPF 控件库——仿制Chrome的ColorPicker
WPF 控件库系列博文地址: WPF 控件库——仿制Chrome的ColorPicker WPF 控件库——仿制Windows10的进度条 WPF 控件库——轮播控件 WPF 控件库——带有惯性的Sc ...
- WPF 控件库——轮播控件
WPF 控件库系列博文地址: WPF 控件库——仿制Chrome的ColorPicker WPF 控件库——仿制Windows10的进度条 WPF 控件库——轮播控件 WPF 控件库——带有惯性的Sc ...
- WPF 控件库——带有惯性的ScrollViewer
WPF 控件库系列博文地址: WPF 控件库——仿制Chrome的ColorPicker WPF 控件库——仿制Windows10的进度条 WPF 控件库——轮播控件 WPF 控件库——带有惯性的Sc ...
- 《Dotnet9》系列-开源C# WPF控件库3《HandyControl》强力推荐
大家好,我是Dotnet9小编,一个从事dotnet开发8年+的程序员.我最近开始写dotnet分享文章,希望能让更多人看到dotnet的发展,了解更多dotnet技术,帮助dotnet程序员应用do ...
- 我的WPF控件库——KAN.WPF.XCtrl(141105)
自己开发的WPF控件库,只是初版,有扩展的Button,TextBox,Window.详细参见前几篇博文. WPF自定义控件(一)——Button:http://www.cnblogs.com/Qin ...
- 国内开源C# WPF控件库Panuon.UI.Silver推荐
国内优秀的WPF开源控件库,Panuon.UI的优化版本.一个漂亮的.使用样式与附加属性的WPF UI控件库,值得向大家推荐使用与学习. 今天站长(Dotnet9,站长网址:https://dotne ...
- 国内开源C# WPF控件库Panuon.UI.Silver强力推荐
国内优秀的WPF开源控件库,Panuon.UI的优化版本.一个漂亮的.使用样式与附加属性的WPF UI控件库,值得向大家推荐使用与学习. 今天站长(Dotnet9,站长网址:https://dotne ...
随机推荐
- linux下通过sysfs操作GPIO
linux下通过sysfs操作GPIO 在嵌入式设备中对GPIO的操作是最基本的操作.一般的做法是写一个单独驱动程序,网上大多数的例子都是这样的.其实linux下面有一个通用的GPIO操作接口,那就是 ...
- Spring Cloud Eureka 注册,发布,调用服务
Spring Cloud为服务治理做了一层抽象接口,所以在Spring Cloud应用中可以支持多种不同的服务治理框架,如Netflix Eureka.Consul和Zookeeper.在Spring ...
- Vue.js:组件
ylbtech-Vue.js:组件 1.返回顶部 1. Vue.js 组件 组件(Component)是 Vue.js 最强大的功能之一. 组件可以扩展 HTML 元素,封装可重用的代码. 组件系统让 ...
- 1134 Vertex Cover
题意:给出一个图和k个查询,每个查询给出Nv个结点,问与这些结点相关的边是否包含了整个图的所有边. 思路:首先,因为结点数较多,用邻接表存储图,并用unordered_map<int,unord ...
- 1078 Hashing
题意:给出表长和待插入的元素,求每个元素的插入位置,散列函数为H(key)=key%TSize,解决冲突利用平方探测法(只考虑正向偏移). 思路:同1145 Hashing - Average Sea ...
- 从request获取各种路径总结 request.getRealPath("url")
转载:http://blog.csdn.net/piaoxuan1987/article/details/8541839 equest.getRealPath() 这个方法已经不推荐使用了,代替方法是 ...
- java成神之——MySQL Connector/J 的基本使用
使用示例 DBCP连接池 结语 使用示例 public class demo { static Connection con = null; static Statement st = null; s ...
- flask ---映射到数据库
在当前项目文件下:运行cmd指令(terminal中) (1)python manage.py db init ----初始化文件 (2)python manage.py db migrate-- ...
- Springboot项目打成jar包运行 和 打成war包 外部tomcat运行
Jar打包方式运行 类型为jar时 <packaging>jar</packaging> 1.使用命令mvn clean package 打包 2.使用java –jar 包 ...
- 并发模型(一)——Future模式
多线程开发可以更好的发挥多核cpu性能,常用的多线程设计模式有:Future.Master-Worker.Guard Susperionsion.不变.生产者-消费者 模式: jdk除了定义了若干并发 ...