天天看點

WPF 控件庫——可拖動頁籤的TabControl

原文: WPF 控件庫——可拖動頁籤的TabControl 一、先看看效果

WPF 控件庫——可拖動頁籤的TabControl

二、原理

1、頁籤大小和位置

  這次給大家介紹的控件是比較常用的TabControl,網上常見的TabControl樣式有很多,其中一部分也支援拖動頁籤,但是帶動畫效果的很少見。這也是有原因的,因為想要做一個不失原有功能,還需要添加動畫效果的控件可不是一行代碼的事。要做成上圖中的效果,我們不能一蹴而就,最忌諱的是一上來就想實作所有效果。

  一開始,我們最好先用Blend看看原生的TabControl樣式模闆部分是如何實作的,這樣我們也好有個參考。我們先從資産面闆中拖一個TabControl放到窗體中,調整好合适的大小:

WPF 控件庫——可拖動頁籤的TabControl
  然後在它上面右鍵,編輯模闆->編輯副本->确定,在自動生成的xaml代碼中關鍵部分是這裡:
WPF 控件庫——可拖動頁籤的TabControl
  可以看到,所有的頁籤(也就是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);      

  因為頁籤左右都有邊距,減去一個左邊距,兩者間的間隔就是一個邊距了。

  頁籤大小和位置的邏輯處理大緻是上述的過程,由于篇幅有限,加之我不喜歡一貼一大段代碼,是以隻挑重點來讨論,完整的代碼還要考慮各種情況,這裡就不再贅述了。

2、動畫處理

  這一部分我們的關注點就是滑鼠了,對頁籤而言,滑鼠按下、滑鼠移動、滑鼠擡起,這些我們都要關注,是以分别給它們訂閱一下事件。與之對應的,我們還要給頁籤添加幾個标私有字段,用以記錄狀态,比如 _isDragging 、 _isDragged 、 _dragPoint 、 _isWaiting ,前兩個我就不說了,都是字面意思,第三個則用來暫存滑鼠移動時的位置,每次進入頁籤的 OnMouseMove 事件,都要将 _isDragged 和其舊值作差,以求得目前頁籤應該移動的距離。 _isWaiting 用途比較特殊,在使用者拖動頁籤時,我們最好等待一個粘滞距離,比如20個機關寬度,也就是說,在水準方向滑鼠移動了超過20個像素無關機關後,頁籤才開始被拖動。

  在一開始的gif中可以看到,被拖動的頁籤改變位置時,其餘的頁籤也會動态改變位置,那麼位置改變的時機是如何确定的呢?很簡單,隻要将被拖動的頁籤到容器(TabPanel)左邊界的這個距離除以 ItemWidth ,結果四舍五入就是這個頁籤目前應該所處的位置,緊接着下一步就是要把這個位置上的頁籤和目前被拖動的換個位置。此刻我們終于可以用動畫來實作了,由于這個系列的文章多次講過動畫的代碼了,是以就不再贅述。

  上面一段講的是換位置,那麼添加頁籤、删除頁籤呢?其實有個捷徑可以走,就是使用 FluidMoveBehavior ,把他往樣式裡一塞,好了,效果出來了!

WPF 控件庫——可拖動頁籤的TabControl

  但是這裡有一個坑要注意, FluidMoveBehavior 雖然可以化簡一部分動畫邏輯,但是它有點越權了,它把你位置移動的邏輯也給做了,你會發現,如果不加處理,在你自己的動畫結束後它還會再來一遍它的動畫。可以将 FluidMoveBehavior 的 Duration 屬性暫時歸零來解決這個問題: FluidMoveDuration = new Duration(TimeSpan.FromSeconds(0)); 。

  這篇文章隻是大緻介紹一下實作的過程和思路,感興趣的可以下載下傳源碼,多多交流,共同提高。

三、源碼

  本文所讨論的控件源碼已經在github開源:

https://github.com/NaBian/HandyControl