[WPF]原生TabControl控制元件實現拖拽排序功能

2023-10-24 15:00:17

在UI互動中,拖拽操作是一種非常簡單友好的互動。尤其是在ListBox,TabControl,ListView這類列表控制元件中更為常見。通常要實現拖拽排序功能的做法是自定義控制元件。本文將分享一種在原生控制元件上設定附加屬性的方式實現拖拽排序功能。

該方法的使用非常簡單,僅需增加一個附加屬性就行。

<TabControl
    assist:SelectorDragDropAttach.IsItemsDragDropEnabled="True"
    AlternationCount="{Binding ClassInfos.Count}"
    ContentTemplate="{StaticResource contentTemplate}"
    ItemContainerStyle="{StaticResource TabItemStyle}"
    ItemsSource="{Binding ClassInfos}"
    SelectedIndex="0" />

實現效果如下:

主要思路

WPF中核心基礎類別UIElement包含了DragEnterDragLeaveDragEnterDrop等拖拽相關的事件,因此只需對這幾個事件進行監聽並做相應的處理就可以實現WPF中的UI元素拖拽操作。

另外,WPF的一大特點是支援資料驅動,即由資料模型來推動UI的呈現。因此,可以通過通過拖拽事件處理拖拽的源位置以及目標位置,並獲取到對應位置渲染的資料,然後運算元據集中資料的位置,從而實現資料和UI介面上的順序更新。

首先定義一個附加屬性類SelectorDragDropAttach,通過附加屬性IsItemsDragDropEnabled控制是否允許拖拽排序。

public static class SelectorDragDropAttach
{
    public static bool GetIsItemsDragDropEnabled(Selector scrollViewer)
    {
        return (bool)scrollViewer.GetValue(IsItemsDragDropEnabledProperty);
    }

    public static void SetIsItemsDragDropEnabled(Selector scrollViewer, bool value)
    {
        scrollViewer.SetValue(IsItemsDragDropEnabledProperty, value);
    }

    public static readonly DependencyProperty IsItemsDragDropEnabledProperty =
        DependencyProperty.RegisterAttached("IsItemsDragDropEnabled", typeof(bool), typeof(SelectorDragDropAttach), new PropertyMetadata(false, OnIsItemsDragDropEnabledChanged));

    private static readonly DependencyProperty SelectorDragDropProperty =
        DependencyProperty.RegisterAttached("SelectorDragDrop", typeof(SelectorDragDrop), typeof(SelectorDragDropAttach), new PropertyMetadata(null));

    private static void OnIsItemsDragDropEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        bool b = (bool)e.NewValue;
        Selector selector = d as Selector;
        var selectorDragDrop = selector?.GetValue(SelectorDragDropProperty) as SelectorDragDrop;
        if (selectorDragDrop != null)
            selectorDragDrop.Selector = null;
        if (b == false)
        {
            selector?.SetValue(SelectorDragDropProperty, null);
            return;
        }
        selector?.SetValue(SelectorDragDropProperty, new SelectorDragDrop(selector));

    }

}

其中SelectorDragDrop就是處理拖拽排序的物件,接下來看下幾個主要事件的處理邏輯。
通過PreviewMouseLeftButtonDown確定選中的需要拖拽操作的元素的索引

void selector_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    if (this.IsMouseOverScrollbar)
    {
        //Set the flag to false when cursor is over scrollbar.
        this.canInitiateDrag = false;
        return;
    }

    int index = this.IndexUnderDragCursor;
    this.canInitiateDrag = index > -1;

    if (this.canInitiateDrag)
    {
        // Remember the location and index of the SelectorItem the user clicked on for later.
        this.ptMouseDown = GetMousePosition(this.selector);
        this.indexToSelect = index;
    }
    else
    {
        this.ptMouseDown = new Point(-10000, -10000);
        this.indexToSelect = -1;
    }
}

PreviewMouseMove事件中根據需要拖拽操作的元素建立一個AdornerLayer,實現滑鼠拖著元素移動的效果。其實拖拽移動的只是這個AdornerLayer,真實的元素並未移動。

void selector_PreviewMouseMove(object sender, MouseEventArgs e)
{
    if (!this.CanStartDragOperation)
        return;

    // Select the item the user clicked on.
    if (this.selector.SelectedIndex != this.indexToSelect)
        this.selector.SelectedIndex = this.indexToSelect;

    // If the item at the selected index is null, there's nothing
    // we can do, so just return;
    if (this.selector.SelectedItem == null)
        return;

    UIElement itemToDrag = this.GetSelectorItem(this.selector.SelectedIndex);
    if (itemToDrag == null)
        return;

    AdornerLayer adornerLayer = this.ShowDragAdornerResolved ? this.InitializeAdornerLayer(itemToDrag) : null;

    this.InitializeDragOperation(itemToDrag);
    this.PerformDragOperation();
    this.FinishDragOperation(itemToDrag, adornerLayer);
}

DragEnterDragLeaveDragEnter事件中處理AdornerLayer的位置以及是否顯示。

Drop事件中確定了拖拽操作目標位置以及渲染的資料元素,然後移動後設資料,通過資料順序的變化更新介面的排序。從程式碼中可以看到列表控制元件的ItemsSource不能為空,否則拖拽無效。這也是後邊將提到的一個缺點。

void selector_Drop(object sender, DragEventArgs e)
{
    if (this.ItemUnderDragCursor != null)
        this.ItemUnderDragCursor = null;

    e.Effects = DragDropEffects.None;

    var itemsSource = this.selector.ItemsSource;
    if (itemsSource == null) return;

    int itemsCount = 0;
    Type type = null;
    foreach (object obj in itemsSource)
    {
        type = obj.GetType();
        itemsCount++;
    }

    if (itemsCount < 1) return;
    if (!e.Data.GetDataPresent(type))
        return;

    object data = e.Data.GetData(type);
    if (data == null)
        return;

    int oldIndex = -1;
    int index = 0;
    foreach (object obj in itemsSource)
    {
        if (obj == data)
        {
            oldIndex = index;
            break;
        }
        index++;
    }
    int newIndex = this.IndexUnderDragCursor;

    if (newIndex < 0)
    {
        if (itemsCount == 0)
            newIndex = 0;
        else if (oldIndex < 0)
            newIndex = itemsCount;
        else
            return;
    }
    if (oldIndex == newIndex)
        return;

    if (this.ProcessDrop != null)
    {
        // Let the client code process the drop.
        ProcessDropEventArgs args = new ProcessDropEventArgs(itemsSource, data, oldIndex, newIndex, e.AllowedEffects);
        this.ProcessDrop(this, args);
        e.Effects = args.Effects;
    }
    else
    {
        dynamic dItemsSource = itemsSource;
        if (oldIndex > -1)
            dItemsSource.Move(oldIndex, newIndex);
        else
            dItemsSource.Insert(newIndex, data);
        e.Effects = DragDropEffects.Move;
    }
}

優點與缺點

優點:

  • 用法簡單,封裝好拖拽操作的附加屬性後,只需一行程式碼實現拖拽功能。
  • 對現有專案友好,對於已有專案需要擴充套件拖拽操作排序功能,無需替換控制元件。
  • 支援多種列表控制元件擴充套件。派生自SelectorListBoxTabControlListView,ComboBox都可使用該方法。

缺點:

  • 僅支援通過資料繫結動態渲染的列表控制元件,XAML寫死或者後臺程式碼迴圈新增列表元素建立的列表控制元件不適用該方法。
  • 僅支援列表控制元件內的元素拖拽,不支援穿梭框拖拽效果。
  • 不支援同時拖拽多個元素。

小結

本文介紹列表拖拽操作的解決方案不算完美,功能簡單但輕量,並且很好的體現了WPF的資料驅動的思想。個人非常喜歡這種方式,它能讓我們輕鬆的實現列表資料的增刪以及排序操作,而不是耗費時間和精力去自定義可增刪資料的控制元件。

參考

https://www.codeproject.com/Articles/17266/Drag-and-Drop-Items-in-a-WPF-ListView

程式碼範例

SelectorDragDropSamples