底部附有Demo示例。需要的朋友可以去下载参考
一、图示
先上图,不知为啥,GIF总看起来特别卡,实际却很流畅。
由于录制问题,GIF动画只会播放一次,需要重复观看的,请将网页关闭后重新打开再观看
WPF的资料太少了,找些动画什么的都没有,最近工作中要用Treeview进行拖动排序,但是呢,网上几乎没有相关的Demo示例等,只能自己手撸,在这过程中,也学到了一些知识,我在此记录下,防止下次用到,或者需要的小伙伴用到。
二、 原理
我是利用控件的平移实现拖动效果的。当平移到边界的时候,会触发TreeView的上下滚动(滚动的时候也会平移);脱离边界则停止滚动;滚到最上和最下时,停止平移;当手指抬起来时,捕获当前鼠标的位置,将拖动的Item插入到当前鼠标位置的Item前面,同时删除数据集合旧的数据,添加新的数据到添加的位置
技术点:
1. TreeView代码控制滚动
2. 控件拖动,平移
3. 获取控件在其他父控件中的相对位置
4. 利用鼠标事件做平移,和拖动结束后的排序
三、代码解析
首先我们将布局写好,布局就是一个普通的TreeView。不过我们需要给控件添加一个MouseMove事件,该事件用来判断是否鼠标移动到了边界,然后做响应的自动滚动动作。
<Window x:Class="WpfAppTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfAppTest"
mc:Ignorable="d"
Loaded="OnViewLoaded"
Title="MainWindow" Height="450" Width="800">
<Grid>
<TreeView x:Name="mTreeView" Width="300" HorizontalAlignment="Left" MouseMove="ParentMove"/>
</Grid>
</Window>
接下来在初始化的时候,我们将TreeView的数据添加到Dictionary里面,再根据Dictionary的数据初始化TreeView的Item。
private void initData()
{
datas["A"] = new List<string>();
datas["A"].Add("阿轲");
datas["A"].Add("艾琳");
datas["A"].Add("安其拉");
datas["B"] = new List<string>();
datas["B"].Add("白起");
datas["B"].Add("百里守约");
datas["B"].Add("扁鹊");
datas["B"].Add("百里玄策");
datas["B"].Add("不知火舞");
datas["C"] = new List<string>();
datas["C"].Add("蔡文姬");
datas["C"].Add("曹操");
datas["C"].Add("陈咬金");
datas["C"].Add("嫦娥");
datas["C"].Add("成吉思汗");
datas["D"] = new List<string>();
datas["D"].Add("狄仁杰");
datas["D"].Add("典韦");
datas["D"].Add("貂蝉");
datas["D"].Add("盾山");
datas["D"].Add("东皇太一");
datas["D"].Add("妲己");
datas["D"].Add("大乔");
datas["D"].Add("达摩");
datas["G"] = new List<string>();
datas["G"].Add("宫本武藏");
datas["G"].Add("高渐离");
datas["G"].Add("鬼谷子");
datas["G"].Add("干将莫邪");
datas["G"].Add("关羽");
datas["G"].Add("公孙离");
datas["H"] = new List<string>();
datas["H"].Add("后裔");
datas["H"].Add("韩信");
datas["H"].Add("花木兰");
datas["H"].Add("黄忠");
}
private void initViews()
{
foreach (KeyValuePair<string, List<string>> kv in datas)
{
TreeViewItem parentItem = new TreeViewItem();
parentItem.Header = kv.Key;
parentItem.Tag = kv.Value;
mTreeView.Items.Add(parentItem);
foreach (string info in kv.Value)
parentItem.Items.Add(creatItem(info));
}
}
private TreeViewItem creatItem(object header)
{
TreeViewItem newItem = new TreeViewItem();
newItem.Header = header;
newItem.PreviewMouseDown += ItemMouseDown;
newItem.PreviewMouseMove += ItemMove;
newItem.PreviewMouseUp += ItemMouseUp;
newItem.Height = 30;
return newItem;
}
视图和数据准备好后,旧开始准备做拖动的动作,我上面给Item添加了3个事件,分别是PreviewMouseDown,PreviewMouseMove,PreviewMouseUp。这3个事件就是我们拖动Item的关键了
PreviewMouseDown:获取鼠标点击位置,作为移动的初始位置。初始化平移对象transform
PreviewMouseMove:根据鼠标的移动,对Item进行平移
PreviewMouseUp:鼠标抬起时捕获要插入的位置的Item,并做Item插入
上面我们不是还添加了TreeView的MouseMove事件吗,这个事件主要是边缘检测,也会在这里开启自动滚动
PreviewMouseDown事件的代码比较简单,lastPosition记录了上次移动的坐标,用来计算鼠标移动了多远,我们就将Item移动多远。_isMouseDown是拖动的标记,只有按下时,才会计算鼠标移动的距离,transform是我们用来拖动Item的对象
private void ItemMouseDown(object sender, MouseButtonEventArgs e)
{
lastPosition = 0;
var c = sender as UIElement;
_isMouseDown = true;
TreeViewItem treeViewItem = (TreeViewItem)sender;
mouseDownOffset = e.GetPosition(treeViewItem).Y + 5;
_mouseDownPosition = e.GetPosition(this);
var transform = c.RenderTransform as TranslateTransform;
if (transform == null)
{
transform = new TranslateTransform();
c.RenderTransform = transform;
}
c.CaptureMouse();
}
PreviewMouseMove事件计算鼠标移动的距离,然后将Item也拖动响应的距离。然后将本次的Y轴位置服给lastPosition,以便下次计算使用。
e.GetPosition(UIElement) 是获取鼠标在控件中的位置,要要获取哪个控件的相对位置,就控件作为参数传给GetPosition,就可以获取位置。Position和Position之间可以做加减法,计算两个坐标之间的直线距离
if (_isMouseDown && !isRollingItem[0])
{
TreeViewItem viewItem = (TreeViewItem)sender;
dragingItemHeight = viewItem.ActualHeight;
var c = sender as UIElement;
var pos = e.GetPosition(this);
var dp = pos - _mouseDownPosition;
transform = c.RenderTransform as TranslateTransform;
//transform.X = _mouseDownControlPosition.X + dp.X;
//transform.Y = _mouseDownControlPosition.Y + dp.Y;
transform.Y = transform.Y + (dp.Y - lastPosition);
lastPosition = dp.Y;
}
PreviewMouseUp事件就直接计算当前位置,将拖动的Item插入到当前位置。
我在项目中使用控件时,发现向上拖动会在其他Item上面,向下拖动会在Item底下(被遮挡)。当然我的Item是我自定义的布局。 当鼠标弹起时,并不能及时捕获鼠标在哪个Item上停留,需要等待大约100毫秒后才能获取到。所以遇到问题还得变通下
下面我们看下TreeView自动滚动的代码,这个比较重要,需要TreeView滚动的同时,拖动的Item也要平移
private void ScrollingOnMainThread(object obj)
{
bool isDownScroll = (bool)obj;
if (isDownScroll)
{
if (scrollViewer.VerticalOffset >= (scrollViewer.ExtentHeight - mTreeView.ActualHeight)) return;
scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset + 6); //向下调节垂直滚动条的位置;
transform.Y += 6;
}
else
{
if (scrollViewer.VerticalOffset <= 0) return;
scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - 6); //向上调节垂直滚动条的位置;
transform.Y -= 6;
}
}
重要的也就这些,现在你的控件应该也能动起来了。
如果你还需要demo示例参考或查看效果。
本文代码实现简单,扩展性强,Demo示例总共也就267行,代码还包含了数据初始化,View控件初始化等非逻辑代码,代码简单以维护。
如果在使用中有不懂的,您还可以通过私信询问,我经常在线的,帮您解答使用中的困难。 Demo示例有可能不太完善,如果您发现问题,可以提出来大家共同学习,将项目完善起来,本文试我手撸的代码,如果您喜欢,还请点赞支持下。