1. 程式人生 > >WPF 控制元件庫——可拖動選項卡的TabControl

WPF 控制元件庫——可拖動選項卡的TabControl

一、先看看效果

二、原理

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 = 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 ,把他往樣式裡一塞,好了,效果出來了!

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

  這篇文章只是大致介紹一下實現的過程和思路,感興趣的可以下載原始碼,多多交流,共同提高。

三、原始碼