这个功能是为另一个小工具作基础实现用的
参考了:

WPF树控件实现节点拖动功能
WPF学习- AllowDrop 用户控件启用拖放功能

一.使用事件自实现

1.先上全部代码

Xml代码-点击展开
<Window x:Class="WpfApp1.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:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <HierarchicalDataTemplate x:Key="TreeViewDropItemTemplate" ItemsSource="{Binding Childern}">
            <Grid  MouseLeftButtonDown="mouseDown"  MouseLeftButtonUp="mouseUp">
                <TextBlock  Name="TV_Tb" Grid.Column="1" Height="20" Text="{Binding TextName}" FontSize="16" HorizontalAlignment="Center"/>
                <Rectangle  Name="Rect"
                        MouseEnter="mouseEnter"
                        MouseLeave="mouseLeave"
                        Height="5" Width="auto" VerticalAlignment="Bottom" Fill="#00292929" Visibility="Visible" />
                <Rectangle  Name="Rect2" Height="1" Width="auto" VerticalAlignment="Bottom" Fill="#FF5489C9" Visibility="Collapsed" />
            </Grid>
            <HierarchicalDataTemplate.Triggers>
                <DataTrigger Binding="{Binding Path=Add}" Value="true" >
                    <Setter TargetName="Rect2" Property="Visibility" Value="Visible"/>
                </DataTrigger>
            </HierarchicalDataTemplate.Triggers>
        </HierarchicalDataTemplate>
    </Window.Resources>
    <Grid Margin="50">
        <Border Background="#FFC7C7C7">
            <TreeView x:Name="treeView"   Width="240" ItemTemplate="{DynamicResource TreeViewDropItemTemplate}" HorizontalAlignment="Left"></TreeView>

        </Border>
    </Grid>
</Window>
CS后台代码-点击展开
    public class TreeItem : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public string TextName { get; set; }

        /// <summary>
        /// 父节点
        /// </summary>
        public TreeItem Parent { get; set; }

        /// <summary>
        /// 子节点集合(若集合保持Null,则在new后,页面不会更新,所以初始化不为Null)
        /// </summary>
        public ObservableCollection<TreeItem> Childern { get; set; } = new ObservableCollection<TreeItem>();

        bool u_Add;
        public bool Add
        {
            get => u_Add;
            set
            {
                u_Add = value;
                if (PropertyChanged != null)
                    PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Add"));
            }
        }
    }

    public partial class MainWindow : Window
    {
        ObservableCollection<TreeItem> treeItemlist = new ObservableCollection<TreeItem>();
        public MainWindow()
        {
            InitializeComponent();
            initTree();
        }

        TreeItem moveItem = null;

        void mouseDown(object sender, MouseEventArgs e)
        {
            var obj = e.OriginalSource as FrameworkElement;
            var data = obj.DataContext as TreeItem;

            moveItem = data;
        }

        void mouseUp(object sender, MouseEventArgs e)
        {
            var isadd = false;

            if (e.OriginalSource is TextBlock) isadd = true;

            var obj = e.OriginalSource as FrameworkElement;
            var data = obj.DataContext as TreeItem;

            if (moveItem.TextName == data.TextName)
                return;

            var oldParChi = moveItem.Parent?.Childern ?? treeItemlist;
            if (oldParChi == null)
                throw new Exception("旧子树空异常");
            oldParChi.Remove(moveItem);

            //判断为兄弟
            if (isadd == false)
            {
                var list = data.Parent?.Childern ?? treeItemlist;
                var borIndex = list.IndexOf(data) + 1;

                moveItem.Parent = data.Parent;
                list.Insert(borIndex, moveItem);
            }

            //新节点
            if (isadd == true)
            {
                moveItem.Parent = data;
                data.Childern.Add(moveItem);
            }

            moveItem = null;
            data.Add = false;
        }


        void mouseEnter(object sender, MouseEventArgs e)
        {
            if (moveItem == null)
                return;

            var obj = e.OriginalSource as FrameworkElement;
            var data = obj.DataContext as TreeItem;


            if (moveItem.TextName == data.TextName)
                return;

            data.Add = true;
        }

        void mouseLeave(object sender, MouseEventArgs e)
        {
            if (moveItem == null)
                return;

            var obj = e.OriginalSource as FrameworkElement;
            var data = obj.DataContext as TreeItem;

            data.Add = false;
        }


        private void initTree()
        {
            var root1 = new TreeItem
            {
                TextName = "root1"
            };
            var root2 = new TreeItem
            {
                TextName = "root2"
            };
            var root3 = new TreeItem
            {
                TextName = "root3",
                Childern = new ObservableCollection<TreeItem> { new TreeItem { TextName = "233" } }
            };

            foreach (var item in root3.Childern)
            {
                item.Parent = root3;
            }
            treeItemlist.Add(root1);
            treeItemlist.Add(root2);
            treeItemlist.Add(root3);
            treeView.ItemsSource = treeItemlist;
        }
    }

2.谈谈思路

基于组件TreeView,所以Grid内只有TreeView
并其内容使用自定义的模板TreeViewDropItemTemplate,并设置他们绑定的数据TreeViewDropItemTemplateChildern
Triggers是用于绑定了Add属性,对Rectangle中的Visibility进行是否显示控制
两个Rectangle分别作为鼠标移动的下划线事件触发,下划线则为加到当前节点下面否则为加到目标节点的中;第二个下划线为在目标节点的下划线样式实现

1.在按下树节点后,记录按下的节点信息,并在鼠标抬起时,获取抬起的目标控件和节点信息
2.移除对应的父节点的自己节点,并将父节点指向新的父节点,在新的父节点中添加自己到其子节点列中

二.继承用户组件,重写已有事件

1.先上全部代码:

MainXml代码-点击展开
<Window x:Class="WpfApp1.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:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid Margin="50" >
        <TreeView x:Name="tree" AllowDrop="True">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate DataType="{x:Type local:TreeItem}" ItemsSource="{Binding Childern}">
                    <local:UserControl1 ></local:UserControl1>
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
    </Grid>
</Window>
自定义组件代码-点击展开
<UserControl x:Class="WpfApp1.UserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfApp1"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800" AllowDrop="True">
    <Grid >
        <TextBlock Grid.Column="1" Height="20" Text="{Binding TextName}" FontSize="16" HorizontalAlignment="Center"/>
        <Grid  Height="5" VerticalAlignment="Bottom" Background="#00E87474">
            <Rectangle Height="1" Width="auto" VerticalAlignment="Bottom" Fill="#FF5489C9">
                <Rectangle.Style>
                    <Style TargetType="Rectangle">
                        <Setter Property="Visibility" Value="Collapsed" />
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding Path=Add}" Value="True" >
                                <Setter  Property="Visibility" Value="Visible"/>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </Rectangle.Style>
            </Rectangle>
        </Grid>
    </Grid>
</UserControl>

CS后台代码-点击展开
public partial class UserControl1 : UserControl
{
    TreeItem _treeItem;
    TreeItem treeItem
    {
        get
        {
            if (_treeItem == null)
                _treeItem = DataContext as TreeItem;
            return _treeItem;
        }
    }

    public UserControl1()
    {
        InitializeComponent();
    }

    protected override void OnDragEnter(DragEventArgs e)
    {
        base.OnDragEnter(e);

        var data = e.Data.GetData("TreeItemModel") as TreeItem;
        var tagElement = e.OriginalSource as FrameworkElement;
        var tagData = tagElement.DataContext as TreeItem;

        var isAdd = e.OriginalSource is Rectangle || e.OriginalSource is Grid;
        if (isAdd && IsAddChi(data, tagData) == false)
            treeItem.Add = true;

        e.Effects = DragDropEffects.Move;

        if (IsAddChi(data, tagData))
            e.Effects = DragDropEffects.None;

        e.Handled = true;
    }

    protected override void OnDragLeave(DragEventArgs e)
    {
        base.OnDragLeave(e);
        treeItem.Add = false;

        var data = e.Data.GetData("TreeItemModel") as TreeItem;
        var tagElement = e.OriginalSource as FrameworkElement;
        var tagData = tagElement.DataContext as TreeItem;

        e.Effects = DragDropEffects.Move;
        if (IsAddChi(data, tagData))
            e.Effects = DragDropEffects.None;

        e.Handled = true;
    }

    protected override void OnDrop(DragEventArgs e)
    {
        var data = e.Data.GetData("TreeItemModel") as TreeItem;
        var tagElement = e.OriginalSource as FrameworkElement;
        var tagData = tagElement.DataContext as TreeItem;

        if (IsAddChi(data, tagData))
            return;

        var isAddIn = false;
        if (e.OriginalSource is TextBlock)
            isAddIn = true;

        var oldParChi = data.Parent?.Childern ?? data.Root;
        if (oldParChi == null)
            throw new Exception("旧子树空异常");

        oldParChi.Remove(data);

        //判断为兄弟
        if (isAddIn == false)
        {
            var list = tagData.Parent?.Childern ?? tagData.Root;
            var borIndex = list.IndexOf(tagData) + 1;

            data.Parent = tagData.Parent;
            list.Insert(borIndex, data);
        }

        //新节点
        if (isAddIn == true)
        {
            data.Parent = tagData;
            tagData.Childern.Add(data);
        }

        if (data.Parent == null)
            data.Root = tagData.Root;

        treeItem.Add = false;
    }
    protected override void OnDragOver(DragEventArgs e)
    {
        var isAdd = e.OriginalSource is Rectangle || e.OriginalSource is Grid;

        if (isAdd == true)
            e.Effects = DragDropEffects.Move;
        else
            e.Effects = DragDropEffects.Copy;

        var data = e.Data.GetData("TreeItemModel") as TreeItem;
        var tagElement = e.OriginalSource as FrameworkElement;
        var tagData = tagElement.DataContext as TreeItem;

        if (IsAddChi(data, tagData))
            e.Effects = DragDropEffects.None;

        e.Handled = true;
    }

    protected override void OnMouseDown(MouseButtonEventArgs e)
    {
        base.OnMouseDown(e);

        var obj = e.OriginalSource as FrameworkElement;
        var data = obj.DataContext as TreeItem;

        DataObject dragData = new DataObject();
        dragData.SetData("TreeItemModel", data);
        dragData.SetData("Object", this);

        DragDrop.DoDragDrop(this, dragData, DragDropEffects.Copy | DragDropEffects.Move);
    }

    bool IsAddChi(TreeItem old, TreeItem @new)
    {
        while (@new.Parent != null)
        {
            if (old.TextName == @new.TextName)
                return true;

            @new = @new.Parent;
        }

        return false;
    }
}

2.谈谈思路

第二种方式主要是利用了自带的AllowDrop拖拽方法,因为我们主要是实现拖拽,所以用Drop的好处是只需要重写事件就行,不需要在前台额外声明事件函数,二是能实现鼠标的形态变化(拖拽文件出现的移动和复制样式)
其他的思路,基本与第一种方式一样
用这种方式,需要注意的是:
1.AllowDrop属性需要打开
2.注意子组件的Triggers写法结构
3.打开拖拽需要执行DragDrop.DoDragDrop(),不然没有拖拽效果

Q.E.D.


随意游世