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 ...
随机推荐
- [转载]amba_device使用分析
什么是AMBA? ---AMBA是一个片内总线规范. ARM官网的介绍:http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0 ...
- 多线程使用信号量sem_init,sem_wait,sem_post
信号量的主要函数有: int sem_init(sem_t *sem,int pshared,unsigned int value); int sem_wait(sem_t *sem); int se ...
- mysql索引之八:myisam压缩(前缀压缩)索引
myisam使用前缀压缩来减少索引的大小,从而让更多的索引可以放入内存中,默认只压缩字符串,但通过参数配置也可以对整数做压缩,myisam压缩每个索引块的方法是,先完全保存索引块中的第一个值,然后将其 ...
- codeforce 980B - Marlin(构造)
Marlin time limit per test 1 second memory limit per test 256 megabytes input standard input output ...
- 1.solr学习速成之配置文件
什么是solr Solr是一个独立的企业级搜索应用服务器,它对外提供类似于Web-service的API接口.用户可以通过http请求,向搜索引擎服务器提交一定格式的XML文件,生成索引:也可以通过H ...
- Oracle11gR2_ADG管理之resinstate实战
主库上打开闪回 SQL> select flashback_on from v$database; FLASHBACK_ON ------------------ YES 模拟断电 SQL> ...
- 关于项目报错Dynamic Web Module 3.0 requires Java 1.6 or newer 的解决方法
起因:今天使用maven创建web项目的时候发现项目上老是有个红X,错误如下图所示,虽然项目能正常运行起来,但是LZ的强迫症发作,不弄掉就觉得心里不舒服,于是上网查了攻略,几经周折最终大功告成,现在就 ...
- eclipse egit(分支管理 上)
这一章比较重要,讲述了Git比svn强大的地方,直接转载廖雪峰老师的文字,更好的理解 什么是分支 和 为什么分支git比svn做的更好 分支在实际中有什么用呢?假设你准备开发一个新功能,但是需要两周才 ...
- Android P2P语音通话实现
1.http://www.cnblogs.com/milospooner/archive/2012/07/13/2590950.html 2.http://my.oschina.net/sanshan ...
- 关于equal和toString方法的实验报告
一 实验目的 了解equal和toString方法 二 实验软件环境 操作系统:windows xp java version: "1.7.0_51" 开发工具:Eclipse S ...