WPF 实现可拖拽调整顺序的ListView自定义控件

前言

想实现一个可拖拽调整ListView中各项顺序的自定义控件,方便后面的功能的实现。

网上没有找到完整的自定义控件的实现思路,但是发现了GongSolutions.WPF.DragDrop,这个项目。发现这个项目基本实现了拖拽需要的功能,使用简单并且非常强大,但是我并不像只因为一点拖拽功能就装一个包。

于是尝试自己简单实现,满足我的基本需求即可的——可以拖拽ListView中每一项ListViewItem,以调整其内部顺序。

思路梳理

简单梳理以下思路,会发现,其主要实现以下几点:

  1. 拖拽开始时,获取被拖拽的ListViewItem
  2. 拖拽过程中,拖拽的样式实现
    1. 经过的ListViewItem显示样式,做出一点改动——就仿佛控件在提示用户,你现在拖拽到我这里了,你放手的话被拖拽的对象会插在我这里。
    2. 被拖拽的对象最好能跟随鼠标移动——让用户明确自己拖拽的哪个ListViewItem
  3. 拖拽完成后,将被拖拽的ListViewItem,顺序调整到鼠标所在ListViewItem所在的位置

难点

其中对于我这个WPF小白来讲的难点在于

  1. 如何获取经过的ListViewItem,即鼠标悬浮下对应的ListViewItem

    使用VisualTreeHelper。VisualTreeHelper提供了在可视树中执行常见的节点操作,比如

var hitTestResult = VisualTreeHelper.HitTest(this, position);
var targetItem = FindAncestor<ListViewItem>((DependencyObject)hitTestResult.VisualHit);
// 此处省略 FindAncestor
获取命中的节点对象,再通过查找父节点直到找到对应的ListViewItem,即可找到对应的ListViewItem。
  1. 如何让被拖拽的ListViewItem随着鼠标移动

    基本思路:使用Popup控件,将ListViewItem设置为其Child,并让它跟随鼠标移动

  2. 如何调整ListView的顺序,即如何调整ItemsSource的顺序

    这里参考了GongSolutions.WPF.DragDrop项目中的方法,通过反射使用ObservableCollection中提供的Move方法,所以这里就限制了ItemSource必须为ObservableCollection类型才支持。

开始实践吧!

经过梳理,我们需要实现两个两个控件

  1. DragListView继承自ListView,扩展其拖拽功能
  2. DragDropPreview继承自Popup,扩展其跟随鼠标移动功能

那么开始实践吧!

DragListView

1. 鼠标点击时,记录开始拖拽的索引、被拖拽的ListViewItem

private void OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    ResetDragDrop();

    var position = e.GetPosition(this);
    var hitTestResult = VisualTreeHelper.HitTest(this, position);
    if (hitTestResult == null) return;
    
    _draggedItem = FindAncestor<ListViewItem>((DependencyObject)hitTestResult.VisualHit);
    if (_draggedItem == null)
    {
        ResetDragDrop();
        return;
    }

    _oldPosition = GetInsertIndex(position);
}

private int GetInsertIndex(Point position)
{
    var hitTestResult = VisualTreeHelper.HitTest(this, position);
    if (hitTestResult == null) return -1;

    var targetItem = FindAncestor<ListViewItem>((DependencyObject)hitTestResult.VisualHit);
    if (targetItem == null) return -1;

    return Items.IndexOf(targetItem.DataContext);
}

private static T FindAncestor<T>(DependencyObject current) where T : DependencyObject
{
    do
    {
        if (current is T)
        {
            return (T)current;
        }
        current = VisualTreeHelper.GetParent(current);
    }
    while (current != null);
    return null;
}


2. 鼠标开始移动时,触发拖拽事件,创建DragDropPreview

 private void OnPreviewDragMove(object sender, MouseEventArgs e)
 {
     if (e.LeftButton == MouseButtonState.Pressed
         && _draggedItem != null && _draggedItemPreview == null)
     {
         CreatePopupWithControl(_draggedItem, _draggedItem.DataContext);
         DragDrop.DoDragDrop(_draggedItem, _draggedItem.DataContext, DragDropEffects.Move);
     }
 }
 
private void CreatePopupWithControl(UIElement control, object data)
{
    var newListItem = new ListViewItem();
    newListItem.Content = data;
    newListItem.Style = this.ItemContainerStyle;
    newListItem.ContentTemplate = this.ItemTemplate;
    
    _draggedItemPreview = new DragDropPreview();
    _draggedItemPreview.PlacementTarget = this;
    _draggedItemPreview.Child = newListItem;
}

 

3. 拖拽时,让DragDropPreview跟随鼠标位置移动

 private void OnDragOver(object sender, DragEventArgs e)
 {
     if (_draggedItemPreview != null && !_draggedItemPreview.IsOpen)
         _draggedItemPreview.IsOpen = true;

     _draggedItemPreview.Move(e.GetPosition(this));
     
     DoDragDropPreviewStyle(e);

 }

4. 拖拽完成后,调整ItemsSource顺序,关闭popup

 private void OnDrop(object sender, DragEventArgs e)
 {
     if (_draggedItem == null) { ResetDragDrop(); return; }
     Move(ItemsSource, _oldPosition, GetInsertIndex(e.GetPosition(this)));

     _draggedItem = null;
     ClosePopup();
 }

DragDropPreview

这里比较简单,我直接参考了`GongSolutions.WPF.DragDrop`项目中的类,做了简化。只提供一个Move方法,支持移动位置即可。

 internal class DragDropPreview : Popup
 {
     public DragDropPreview()
     {
         this.AllowsTransparency = true;
         this.AllowDrop = false;
         this.IsHitTestVisible = false;
         this.Focusable = false;
         this.Placement = PlacementMode.Relative;
         this.StaysOpen = true;
         this.HorizontalAlignment = HorizontalAlignment.Left;
         this.VerticalAlignment = VerticalAlignment.Top;
     }
     public Point Translation { get; }

     public Point AnchorPoint { get; }

     public void Move(Point point)
     {
         var translation = this.Translation;
         var translationX = point.X + translation.X;
         var translationY = point.Y + translation.Y;

         if (this.Child is not null)
         {
             var renderSize = this.Child.RenderSize;

             var renderSizeWidth = renderSize.Width;
             var renderSizeHeight = renderSize.Height;


             if (renderSizeWidth > 0 && renderSizeHeight > 0)
             {
                 var offsetX = renderSizeWidth * -this.AnchorPoint.X;
                 var offsetY = renderSizeHeight * -this.AnchorPoint.Y;

                 translationX += offsetX;
                 translationY += offsetY;
             }
         }

         this.SetCurrentValue(HorizontalOffsetProperty, translationX);
         this.SetCurrentValue(VerticalOffsetProperty, translationY);
     }

 }

扩展

1. 鼠标经过时要ListViewItem时,要增加样式设置

private ListViewItem _previousDashedItem; 
private void DoDragDropPreviewStyle(DragEventArgs e)
{
    // 获取当前鼠标下的ListViewItem
    var position = e.GetPosition(this);
    var hitTestResult = VisualTreeHelper.HitTest(this, position);
    if (hitTestResult == null) return;

    var targetItem = FindAncestor<ListViewItem>((DependencyObject)hitTestResult.VisualHit);

    // 如果鼠标不在任何ListViewItem上或者移动到了新的ListViewItem
    if (targetItem == null || targetItem != _previousDashedItem)
    {
        // 重置上一个ListViewItem的样式
        ResetDragDropPreviewStyle(_previousDashedItem);
        _previousDashedItem = null;
    }

    if (targetItem != null && targetItem != _previousDashedItem)
    {
        // 为新的目标ListViewItem添加样式
        ApplyDragDropPreviewStyle(targetItem, e);
        _previousDashedItem = targetItem;
    }
}
private void ApplyDragDropPreviewStyle(ListViewItem item, DragEventArgs e)
{
    if (item == null) return;
     item.Style = DragDropPreviewStyle;
}

private void ResetDragDropPreviewStyle(ListViewItem item)
{
    if (item == null) return;
    item.Style = null;
}

2. 鼠标上下移动所经过的ListViewItem的样式应当有所不同,比如向上移动时,所经过的ListViewItem应当展示上边框,提示用户,此时防止会插入到改ListViewItem上方,同理,向下移动应当展示下边框。

    实现思路:

    1. 根据鼠标位置移动,判断移动方向

public class MouseTracker
{
    private Point _previousPosition;
    private DateTime _previousTimestamp;

    public MouseTracker(Point point)
    {
        _previousPosition = point;
        _previousTimestamp = DateTime.Now;
    }

    public Orientation GetMouseOrientation(Point currentPosition)
    {
        var deltaX = currentPosition.X - _previousPosition.X;
        var deltaY = currentPosition.Y - _previousPosition.Y;
        var deltaTime = (DateTime.Now - _previousTimestamp).TotalMilliseconds;

        var speedX = deltaX / deltaTime;
        var speedY = deltaY / deltaTime;

        if (RightDragDropPreviewStyleProperty != null && RightDragDropPreviewStyleProperty != null
            && Math.Abs(speedX) > Math.Abs(speedY))
        {
            return speedX > 0 ? Orientation.Right : Orientation.Left;
        }
        else if (UpDragDropPreviewStyleProperty != null && UpDragDropPreviewStyleProperty != null)
        {
            return speedY > 0 ? Orientation.Down : Orientation.Up;
        }
        return Orientation.None;
    }
}

    public enum Orientation
    {
        None,
        Up,
        Down,
        Left,
        Right
    }


    所以只需要在OnPreviewDragMove时,初始化一下`MouseTracker`,在DragOver时就可以获取到鼠标移动方向

 private void OnPreviewDragMove(object sender, MouseEventArgs e)
 {
     if (e.LeftButton == MouseButtonState.Pressed
         && _draggedItem != null && _draggedItemPreview == null)
     {
         _mouseTracker = new MouseTracker(e.GetPosition(this));
         CreatePopupWithControl(_draggedItem, _draggedItem.DataContext);
         DragDrop.DoDragDrop(_draggedItem, _draggedItem.DataContext, DragDropEffects.Move);
     }
 }
 
var orientation = _mouseTracker.GetMouseOrientation(e.GetPosition(this));

    2. 提供上下左右四个Style属性,用户根据需要自定义。


        #region Properties
        public Style UpDragDropPreviewStyle
        {
            get { return (Style)GetValue(UpDragDropPreviewStyleProperty); }
            set { SetValue(UpDragDropPreviewStyleProperty, value); }
        }

        public static readonly DependencyProperty UpDragDropPreviewStyleProperty =
            DependencyProperty.Register("UpDragDropPreviewStyle", typeof(Style), typeof(DragListView), new PropertyMetadata());
        public Style DownDragDropPreviewStyle
        {
            get { return (Style)GetValue(DownDragDropPreviewStyleProperty); }
            set { SetValue(DownDragDropPreviewStyleProperty, value); }
        }

        public static readonly DependencyProperty DownDragDropPreviewStyleProperty =
            DependencyProperty.Register("DownDragDropPreviewStyle", typeof(Style), typeof(DragListView), new PropertyMetadata());
        public Style RightDragDropPreviewStyle
        {
            get { return (Style)GetValue(RightDragDropPreviewStyleProperty); }
            set { SetValue(RightDragDropPreviewStyleProperty, value); }
        }

        public static readonly DependencyProperty RightDragDropPreviewStyleProperty =
            DependencyProperty.Register("RightDragDropPreviewStyle", typeof(Style), typeof(DragListView), new PropertyMetadata());
        public Style LeftDragDropPreviewStyle
        {
            get { return (Style)GetValue(LeftDragDropPreviewStyleProperty); }
            set { SetValue(LeftDragDropPreviewStyleProperty, value); }
        }

        public static readonly DependencyProperty LeftDragDropPreviewStyleProperty =
            DependencyProperty.Register("LeftDragDropPreviewStyle", typeof(Style), typeof(DragListView), new PropertyMetadata());
        #endregion
        
    

    3. 根据移动方向,灵活设置所经过ListViewItem的样式


        private void DoDragDropPreviewStyle(DragEventArgs e)
        {
            // 获取当前鼠标下的ListViewItem
            var position = e.GetPosition(this);
            var hitTestResult = VisualTreeHelper.HitTest(this, position);
            if (hitTestResult == null) return;

            var targetItem = FindAncestor<ListViewItem>((DependencyObject)hitTestResult.VisualHit);

            // 如果鼠标不在任何ListViewItem上或者移动到了新的ListViewItem
            if (targetItem == null || targetItem != _previousDashedItem)
            {
                // 重置上一个ListViewItem的样式
                ResetDragDropPreviewStyle(_previousDashedItem);
                _previousDashedItem = null;
            }

            if (targetItem != null && targetItem != _previousDashedItem)
            {
                // 为新的目标ListViewItem添加样式
                ApplyDragDropPreviewStyle(targetItem, e);
                _previousDashedItem = targetItem;
            }
        }
        private void ApplyDragDropPreviewStyle(ListViewItem item, DragEventArgs e)
        {
            if (item == null) return;
            if (_mouseTracker != null)
            {
                var orientation = _mouseTracker.GetMouseOrientation(e.GetPosition(this));
                if (orientation == Orientation.Down)
                {
                    item.Style = DownDragDropPreviewStyle;
                    return;
                }
                else if (orientation == Orientation.Up)
                {
                    item.Style = UpDragDropPreviewStyle;
                    return;
                }
                else if (orientation == Orientation.Left)
                {
                    item.Style = LeftDragDropPreviewStyle;
                    return;
                }
                else if (orientation == Orientation.Right)
                {
                    item.Style = RightDragDropPreviewStyle;
                    return;
                }
            }
            item.Style = null;
        }

        private void ResetDragDropPreviewStyle(ListViewItem item)
        {
            if (item == null) return;
            item.Style = null;
        }

 使用Demo

<Window
    x:Class="BaseUIDemo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:base="http://Nita/BaseUI"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:BaseUIDemo"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Window.Resources>
        <Style
            x:Key="UpDragDropPreviewStyle"
            TargetType="ListViewItem">
            <Setter Property="BorderThickness" Value="0,2,0,0" />
            <Setter Property="BorderBrush" Value="#31b0cd" />
            <Setter Property="SnapsToDevicePixels" Value="True" />
        </Style>

        <Style
            x:Key="DownDragDropPreviewStyle"
            TargetType="ListViewItem">
            <Setter Property="BorderThickness" Value="0,0,0,2" />
            <Setter Property="BorderBrush" Value="#31b0cd" />
            <Setter Property="SnapsToDevicePixels" Value="True" />
        </Style>
    </Window.Resources>
    <Grid>
        <base:DragListView
            AllowDrop="True"
            DownDragDropPreviewStyle="{StaticResource DownDragDropPreviewStyle}"
            ItemsSource="{Binding Items.Items,
                                  RelativeSource={RelativeSource FindAncestor,
                                                                 AncestorType={x:Type local:MainWindow}}}"
            UpDragDropPreviewStyle="{StaticResource UpDragDropPreviewStyle}">
            <base:DragListView.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock
                            FontSize="13"
                            Text="{Binding Symbol}" />
                        <TextBlock
                            FontSize="13"
                            Text="{Binding Timestamp}" />
                    </StackPanel>
                </DataTemplate>
            </base:DragListView.ItemTemplate>
        </base:DragListView>
    </Grid>
</Window>

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace BaseUIDemo
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();


            var dragDropPreviewStyle = new Style(typeof(ListViewItem));
            dragDropPreviewStyle.Setters.Add(new Setter(Border.BorderThicknessProperty, new Thickness(0, 1, 0, 0)));
            dragDropPreviewStyle.Setters.Add(new Setter(Border.BorderBrushProperty, Brushes.Black));
            dragDropPreviewStyle.Setters.Add(new Setter(Border.SnapsToDevicePixelsProperty, true));
            UpDragDropPreviewStyle = dragDropPreviewStyle;


            var downdragDropPreviewStyle = new Style(typeof(ListViewItem));
            downdragDropPreviewStyle.Setters.Add(new Setter(Border.BorderThicknessProperty, new Thickness(0, 0, 0, 1)));
            downdragDropPreviewStyle.Setters.Add(new Setter(Border.BorderBrushProperty, Brushes.Black));
            downdragDropPreviewStyle.Setters.Add(new Setter(Border.SnapsToDevicePixelsProperty, true));
            DownDragDropPreviewStyle = downdragDropPreviewStyle;
        }

        public Style UpDragDropPreviewStyle
        {
            get { return (Style)GetValue(UpDragDropPreviewStyleProperty); }
            set { SetValue(UpDragDropPreviewStyleProperty, value); }
        }

        public static readonly DependencyProperty UpDragDropPreviewStyleProperty =
            DependencyProperty.Register("UpDragDropPreviewStyle",
                typeof(Style),
                typeof(MainWindow),
                new PropertyMetadata(null));

        public Style DownDragDropPreviewStyle
        {
            get { return (Style)GetValue(DownDragDropPreviewStyleProperty); }
            set { SetValue(DownDragDropPreviewStyleProperty, value); }
        }

        public static readonly DependencyProperty DownDragDropPreviewStyleProperty =
            DependencyProperty.Register("DownDragDropPreviewStyle",
                typeof(Style),
                typeof(MainWindow),
                new PropertyMetadata(null));



        public MainViewModel Items
        {
            get { return (MainViewModel)GetValue(ItemsProperty); }
            set { SetValue(ItemsProperty, value); }
        }

        public static readonly DependencyProperty ItemsProperty =
            DependencyProperty.Register("Items",
                typeof(MainViewModel),
                typeof(MainWindow),
                new PropertyMetadata(new MainViewModel()));



    }
}

效果: 

  • 28
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值