这周所介绍的内容,上周提到了一下,就是节点的拖动,那就动手写吧(建议大家先拉下demo在看文章,demo在文章最后)
关于拖动,一定要设置AllowDrop="true"才行,界面上没什么变化,跟上篇文章一样的,就直接写实现拖动的逻辑吧。
实现拖动的逻辑,我找了很多的资料,最好的一个资料是这WPF Drag and Drop from ListBox into TreeView - Microsoft Q&A(我其实就是按照里面介绍的方法实现的,有两种,大家可以参考),我采用了第二种方法,所以得提前准备好Microsoft.Xaml.Behaviors.Wpf这个包,创建一个名为DragDropBehavior的行为类,用来实现拖动的逻辑,如图
让它继承wpf的核心基类UIElement
接着重写OnAttached这个方法,实现注册事件,我注册了四个事件
这个方法,是来自于它继承的behavior的类,你可以理解为它是个它是一个接口,通过它,你可以
把你想实现的行为动作用在想实现的控件上,类似于组装机器人,现在我们是有了机器人的外壳了,但是还没有给它加行走的程序,这个方法就是实现行走动作的入口。
继续,实现四个方法的逻辑,如图(我一个一个介绍方法要实现什么逻辑)
先定义三个变量,第四个后面再讲
这里也可以用mouseleftbuttondown这个事件进行注册,逻辑很简单,判断左键,点击的元素及绑定的数据源,鼠标点击的坐标(起始坐标)。
鼠标移动,注意需要一个“拦截器”一样功能的变量,不能让拖动还没结束,就产生一个新的拖动事件,这就是 isDragging的作用,定义一个变量用于接收用户选择的节点(selectItem),
(记得创建一个名为Utility的类把获取父控件的方法放进去)判断移动的范围是否大于这个范围(这里可以用数值,也可以用上图的那种,看个人吧),接着就是实现鼠标拖动
还记得我上次定义的树结构,用到的guid吧,是用在拖动这的,关于拖动的规则,可以自行定义,我这里的规则是,root节点可以与root节点进行拖动,但不能root和children进行拖动,而children可以在root,children拖动,大家肯定发现了中间一坨都是if...else...,没办法,我境界还不是很高,不知道怎么优化了,这里需要网友们帮忙了,四个if,从上往下依次是,root拖动到children不会实现效果,root与root直接拖动,root节点自己的拖动(鼠标移动过后有移动到自身),children节点自己的拖动(鼠标移动过后有移动到自身),除上述外,所有的拖动都满足规则。
现在我们去到前端绑定行为
xmlns:behavior="clr-namespace:OptimizedTreeView.Common"
<i:Interaction.Behaviors>
<behavior:DragDropBehavior />
</i:Interaction.Behaviors>
运行发现是可以拖动,只是效果不是很好,第一是拖动不知道自己的鼠标是不是到了目标身上,第二是不知道自己拖动的是哪个节点,所以接下来咱们优化功能。
private void PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
{
var item = (e.OriginalSource as FrameworkElement)?.DataContext as TreeViewModel;
if (item != null)
{
startPoint = e.GetPosition(AssociatedObject);
}
}
}
private void MouseMove(object sender, MouseEventArgs e)
{
if (isDragging)
{
return;
}
var point = e.GetPosition(null);
if (e.LeftButton == MouseButtonState.Pressed)
{
if (sender is TreeView dragSource)
{
if (Math.Abs(point.X - startPoint.X) > System.Windows.SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(point.Y - startPoint.Y) > System.Windows.SystemParameters.MinimumVerticalDragDistance)
{
selectItem = GetDataFromTreeViewItem(dragSource, e.GetPosition(AssociatedObject));
//添加装饰器
//DragDropAdorner dragDropAdorner = new DragDropAdorner((e.OriginalSource as FrameworkElement).Parent as Grid);
//adornerLayer = AdornerLayer.GetAdornerLayer(dragSource);
//adornerLayer.Add(dragDropAdorner);
if (selectItem != null)
{
isDragging = true;
DragDrop.DoDragDrop(dragSource, selectItem.DataContext as TreeViewModel, DragDropEffects.Move);
}
//adornerLayer.Remove(dragDropAdorner);
//adornerLayer = null;
}
}
}
}
private void TreeView_Drop(object sender, DragEventArgs e)
{
if (sender is TreeView)
{
e.Effects = DragDropEffects.None;
e.Handled = true;
TreeViewModel targetItem = (e.OriginalSource as TextBlock)?.DataContext as TreeViewModel;
MainViewModel vm = (sender as TreeView).DataContext as MainViewModel;
if (targetItem != null && vm != null)
{
var result = (TreeViewModel)selectItem.DataContext;
if (targetItem.NodeType == Enums.TreeNodeType.Children && result.NodeType == Enums.TreeNodeType.Root)
{
}
else if (targetItem.NodeType == Enums.TreeNodeType.Root &&
result.NodeType == Enums.TreeNodeType.Root && !targetItem.Equals(result))
{
vm.DeleteCommand.Execute((TreeViewModel)selectItem.DataContext);
result.Parent = targetItem;
targetItem.Children.Add(result);
}
else if (targetItem.NodeType == Enums.TreeNodeType.Root &&
result.NodeType == Enums.TreeNodeType.Root && targetItem.Equals(result))
{
}
else if (targetItem.Id == result.Parent.Id || targetItem.Equals(result))
{
}
else
{
vm.DeleteCommand.Execute((TreeViewModel)selectItem.DataContext);
result.Parent = targetItem;
targetItem.Children.Add(result);
}
}
}
e.Effects = DragDropEffects.Move;
isDragging = false;
//Utility.GetParentObject<Grid>(e.OriginalSource as TextBlock).Background = Brushes.Transparent;
}
private void PreviewQueryContinueDrag(object sender, QueryContinueDragEventArgs e)
{
//adornerLayer.Update();
}
private static TreeViewItem GetDataFromTreeViewItem(TreeView source, Point point)
{
UIElement element = source.InputHitTest(point) as UIElement; //直接获取最底层的元素,这里是textblock
if (element != null)
{
return Utility.GetParentObject<TreeViewItem>(element);//获取父控件
}
return null;
}
优化一:显示是否到了目标身上,为Grid添加两个事件,一个是进入,一个是离开,这里是拖动,所以对应的注册事件也是拖动
DragEnter="Grid_DragEnter" DragLeave="Grid_DragLeave"
private void Grid_DragEnter(object sender, DragEventArgs e)
{
(sender as Grid).Background = Brushes.SkyBlue;
}
private void Grid_DragLeave(object sender, DragEventArgs e)
{
Grid grid = sender as Grid;
grid.Background = Brushes.Transparent;
}
运行测试发现确实可以知道是否拖到目标位置,但是发现移动后放到自身上,也是不变回去,这是因为鼠标移动到自身并没有离开,所有不会触发dragleave事件,导致颜色没变化,因此只能在拖动节点那添加如下代码
Utility.GetParentObject<Grid>(e.OriginalSource as TextBlock).Background = Brushes.Transparent;
优化二:显示自己拖动的节点,需要用到装饰器,官网给出了装饰器的作用(自行去看)
在Utility类中添加一个point变量,一个获取坐标的方法(这里用到window自带的一个dll:user32.dll)导入即可(可以收到导入,或者用特性使用)
public struct POINT { public int X; public int Y; }
[DllImport("user32.dll")]
public static extern bool GetCursorPos(ref POINT point);
接着创建装饰器这个类
继承装饰器,构造函数传入基类,转为wpf元素 (界面展示的那些元素所继承的基类),重写渲染方法,获取鼠标在屏幕上的坐标,转换为在界面上的坐标,获取背景颜色,(自行去官网查看)
接着先绘制背景框,在绘制内容到框里,最后添加,转到DragDropBehavior中(添加一个变量AdornerLayer adornerLayer,在类的最开始地方,我注释那)
修改mosemove中代码,同时实现第四个事件,更新装饰器(不更新,装饰器绘制的内容不会跟随鼠标移动)
运行测试
大致功能都能实现了
补充:
这个拖动,也是有些问题的,我已知的有两个bug,第一个是在两个treeviewitem之间快速点击一个后移到到另外一个装饰显示的是另一个而不是选中的内容,第二个是children与children中拖动(二级拖动到三级),会有问题
解决:对于上述问题,我想到的是第一个问题解决是加定时器,第二个问题解决是需要重构目录树结构,后续我解决了我再更新内容
结束:
本次的内容就到这里,我的文章大部分都是只提供一个思路,实现一些基本的功能,想到用到项目中,不能向我这样写代码,不然会被。。。哈哈哈
好了,不多说了,关于TreeView的系列,我就介绍这么多了,下次我将换内容介绍,如果各位网友有新的需求,可以与我交流。
欢迎各位批评指正,谢谢啦
附赠:
这是我的demo源码:GitHub - TQtong/OptimizedTreeView: 优化目录树,添加了查询和拖动