底部附有Demo示例。需要的朋友可以去下载参考

一、图示

先上图,不知为啥,GIF总看起来特别卡,实际却很流畅。

由于录制问题,GIF动画只会播放一次,需要重复观看的,请将网页关闭后重新打开再观看

WPF DataGrid Item 拖拽 wpf treeview拖拽效果_wpf

 

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示例有可能不太完善,如果您发现问题,可以提出来大家共同学习,将项目完善起来,本文试我手撸的代码,如果您喜欢,还请点赞支持下。