前言
想实现一个可拖拽调整ListView中各项顺序的自定义控件,方便后面的功能的实现。
网上没有找到完整的自定义控件的实现思路,但是发现了GongSolutions.WPF.DragDrop,这个项目。发现这个项目基本实现了拖拽需要的功能,使用简单并且非常强大,但是我并不像只因为一点拖拽功能就装一个包。
于是尝试自己简单实现,满足我的基本需求即可的——可以拖拽ListView中每一项ListViewItem,以调整其内部顺序。
思路梳理
简单梳理以下思路,会发现,其主要实现以下几点:
- 拖拽开始时,获取被拖拽的ListViewItem
- 拖拽过程中,拖拽的样式实现
- 经过的ListViewItem显示样式,做出一点改动——就仿佛控件在提示用户,你现在拖拽到我这里了,你放手的话被拖拽的对象会插在我这里。
- 被拖拽的对象最好能跟随鼠标移动——让用户明确自己拖拽的哪个ListViewItem
- 拖拽完成后,将被拖拽的ListViewItem,顺序调整到鼠标所在ListViewItem所在的位置
难点
其中对于我这个WPF小白来讲的难点在于
-
如何获取经过的ListViewItem,即鼠标悬浮下对应的ListViewItem
使用VisualTreeHelper。VisualTreeHelper提供了在可视树中执行常见的节点操作,比如
var hitTestResult = VisualTreeHelper.HitTest(this, position);
var targetItem = FindAncestor<ListViewItem>((DependencyObject)hitTestResult.VisualHit);
// 此处省略 FindAncestor
获取命中的节点对象,再通过查找父节点直到找到对应的ListViewItem,即可找到对应的ListViewItem。
-
如何让被拖拽的ListViewItem随着鼠标移动
基本思路:使用Popup控件,将ListViewItem设置为其Child,并让它跟随鼠标移动
-
如何调整ListView的顺序,即如何调整ItemsSource的顺序
这里参考了
GongSolutions.WPF.DragDrop
项目中的方法,通过反射使用ObservableCollection中提供的Move方法,所以这里就限制了ItemSource必须为ObservableCollection类型才支持。
开始实践吧!
经过梳理,我们需要实现两个两个控件
- DragListView继承自ListView,扩展其拖拽功能
- 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()));
}
}