• WPF开发快速入门【7】WPF的拖放功能(Drag and Drop)


    概述

    本文描述WPF的拖放功能(Drag and Drop)。

    拖放功能涉及到两个功能,一个就是拖,一个是放。拖放可以发生在两个控件之间,也可以在一个控件自己内部拖放。假设界面上有两个控件,一个TreeView,一个ListView,那么可能发生的拖动有以下几种情况:

    1、TreeView -> ListView

    2、ListView -> TreeView 

    3、TreeView -> TreeView 

    4、ListView -> ListView

    对于拖的控件需要在鼠标移动事件中检测左键按下并启动拖动操作;对于放的控件需要处理Drop等事件来接收数据。如果是在控件内部拖动,则以上两个动作都要处理。

    为简便起见,本文就以ListView拖动到TreeView为例进行讲解。 

     

    在拖与放的控件之间一定会有数据传递,我们可以设计一个类型来进行数据传输,由于ListView本身就是绑定到一个对象列表的,我就把选中的对象字节拿来传递了,没有额外定义类型。

    复制代码
        public class ListViewAdvNodeItem
        {
            public string Title {get;set;}      
         
        }
    复制代码

     listView.ItemsSource的数据类型为:BindableCollection ListViewAdvNodeItems,通过this.listView.SelectedItem可以得到的数据类型即为:ListViewAdvNodeItem

     设计代码如下:

        "listView"                          
                  Mouse.MouseMove="listView_MouseMove" >        
        

     在listView_MouseMove事件中,我们将启动拖动功能。

    复制代码
                  private void listView_MouseMove(object sender, MouseEventArgs e)
           { 
    if (sender is ListView listview

    && e.LeftButton == MouseButtonState.Pressed
    && listview.SelectedItem != null) { DragDrop.DoDragDrop(listview, listview.SelectedItem, DragDropEffects.Move); } }
    复制代码

     通过DragDrop.DoDragDrop方法启动拖动,该方法有三个参数:

    1、发起拖动的控件

    2、传输的数据(这里是一个ListViewAdvNodeItem类型的对象)

    3、拖动的类型,一般为Move或Copy

     

    下面就要在TreeView控件中处理放的事件了

    设计代码:

    复制代码
        "treeView"               
                  AllowDrop="True" 
                  DragDrop.Drop="treeView_Drop"
                  DragDrop.DragOver="treeView_DragOver"
                  DragDrop.DragEnter="treeView_DragEnter"
                  DragDrop.DragLeave="treeView_DragLeave" >   
        
    复制代码

     首先要设置AllowDrop="True",然后重点处理DragDrop.Drop事件:

    复制代码
            private void treeView_Drop(object sender, DragEventArgs e)
            {
                if (e.Data.GetData(typeof(ListViewAdvNodeItem)) is ListViewAdvNodeItem fromListNode)
                {
                    if (e.OriginalSource is TextBlock txtTitle)
                    { if (txtTitle.Tag is Excerpt toExcerpt)
                        {
                            //处理业务
                        }
                    }
                }     
            }
    复制代码

    在处理Drop事件时,我们需要知道两件事情,1:拖来的是什么数据?2、放哪里了?

    首先,通过e.Data.GetData(typeof(ListViewAdvNodeItem))就可以获得数据来源,这里GetData得到的对象就是上面的 listview.SelectedItem;

    其次,通过e.OriginalSource 我们将获得数据放在哪里的问题。这段代码很难理解,要回头看一下TreeView的ItemTemplate定义

    复制代码
            
                "{x:Type local:TreeViewAdvNodeItem}" ItemsSource="{Binding Children}">
                    "itemBorder" Margin="2" BorderBrush="White" BorderThickness="1" >
                        "Horizontal">
                            "nodeImage" Source="../Images/FolderClose.png" Width="20" Height="20"/>
                            "{Binding Title}" Tag="{Binding Excerpt}" VerticalAlignment="Center" FontSize="13" Margin="2"/>
                        
                    
                
            
    复制代码

    从这个模板定义可以看出,TreeView中用来显示Title的控件是一个TextBlock,然后这个TextBlock的Tag属性上还绑定了一个业务对象。

    再回头看上面一段代码,就可以看出具体的逻辑:当鼠标放开时,其所指的对象是一个TextBlock,然后取到这个TextBlock的Tag对象,里面包含了我想要的业务数据。

    到此拖放功能就完成了。

    为了更好的展现效果,我们可以对拖放的目标进行判断,对于一些不能放的位置显示禁止拖放的图标,这时就需要处理DragOver事件了

    复制代码
            private void treeView_DragOver(object sender, DragEventArgs e)
            {   //判断是否允许拖动          
                           e.Effects = DragDropEffects.None;
                if (e.Data.GetData(typeof(ListViewAdvNodeItem)) is ListViewAdvNodeItem fromListNode)
                {
                    if (e.OriginalSource is TextBlock txtTitle)
                    {
                        if (txtTitle.Tag is Excerpt toExcerpt)
                        {
                            if (CanDrop(fromListNode.Excerpt, toExcerpt))  //业务判断
                            {
                                e.Effects = DragDropEffects.Move;
                            }
                        }
                    }               
                } 
                e.Handled = true;
            }
    复制代码

      

    装饰器

    如果拖动时,有下面这样的一个标签跟随鼠标移动,其显示内容是拖动对象的Title,效果就更好了。

    这个就需要通过装饰器来实现。

    关于装饰器的介绍:装饰器概述 - WPF .NET Framework | Microsoft Docs

    首先我们建一个装饰器对象DragTitleAdorner

        public class DragTitleAdorner : Adorner
        {
            private readonly ContentPresenter _contentPresenter;
            private Control Control
            {
                get
                {
                    return (Control)this.AdornedElement;
                }
            }
    
            public DragTitleAdorner(UIElement adornedElement, Point pos, string? Title = "") : base(adornedElement)
            {
                IsHitTestVisible = false;
    
                int width = 22;
                if (Title != null)
                {
                    width += (int)MeasureTextWidth(Title, 14, "宋体");
                }
    
                this._contentPresenter = new ContentPresenter
                {
                    Content = new Border
                    {
                        Background = Brushes.SteelBlue,
                        Width = width,
                        Height = 28,
                        BorderBrush = Brushes.Gray,
                        BorderThickness = new Thickness(1),
                        HorizontalAlignment = HorizontalAlignment.Left,
                        VerticalAlignment = VerticalAlignment.Top,
                        CornerRadius= new CornerRadius(5),
                        Child = new TextBlock
                        {
                            Text = Title,
                            FontSize = 14,
                            FontFamily= new FontFamily("宋体"),
                            Foreground = Brushes.White,
                            HorizontalAlignment = HorizontalAlignment.Left,
                            VerticalAlignment = VerticalAlignment.Center,
                            Margin = new Thickness(10, 0, 0, 0),
                        },
                    },
                };
    
                double left = pos.X;
                double top = pos.Y;
                this.Margin = new Thickness(left + 5, top + 10, 0, 0);
            }
    
            #region Override
    
            protected override int VisualChildrenCount
            {
                get
                {
                    return 1;
                }
            }
    
            protected override Visual GetVisualChild(int index) // replace the Visual of the TextBox with the visual of the _contentPresenter;
            {
                return this._contentPresenter;
            }
    
            protected override Size MeasureOverride(Size constraint)
            {
                this._contentPresenter.Measure(this.Control.RenderSize); // delegate the measure override to the ContentPresenter's Measure
                return this.Control.RenderSize;
            }
    
            protected override Size ArrangeOverride(Size finalSize)
            {
                this._contentPresenter.Arrange(new Rect(finalSize));
                return finalSize;
            }
    
            #endregion Override
    
            private  double MeasureTextWidth(string text, double fontSize, string fontFamily)
            {
                FormattedText formattedText = new FormattedText(
                text,
                System.Globalization.CultureInfo.InvariantCulture,
                FlowDirection.LeftToRight,
                new Typeface(fontFamily.ToString()),
                fontSize,
                Brushes.Black
                );
                return formattedText.WidthIncludingTrailingWhitespace;
            }
    
        }
    View Code

    在构造这个对象时,我们将传入两个重要的参数:Point pos 和 string Title ,这两个参数决定了它在何处显示什么内容。

    程序用代码构建了一个Border,其内有一个TextBlock,并通过pos参数来控制了它的位置。

    下面,在treeView_DragOver事件中显示这个装饰器即可。

    复制代码
            private void treeView_DragOver(object sender, DragEventArgs e)
            {
                if (e.Data.GetData(typeof(ListViewAdvNodeItem)) is ListViewAdvNodeItem fromListNode)
                { 
                    //显示装饰器
                    AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(this.treeView);
                    if (adornerLayer != null)
                    {
                        Adorner[] adorners = adornerLayer.GetAdorners(this.treeView);
                        if (adorners != null)
                        {
                            foreach (var adorner in adorners)
                            {
                                adornerLayer.Remove(adorner);
                            }
                        }
    
                        DragTitleAdorner _adorner = new DragTitleAdorner(this.treeView, pos, fromListNode.Excerpt?.Title);
                        adornerLayer.Add(_adorner);
                    }
                } 
                e.Handled = true;
            }
    复制代码

     更多信息请参考文末源码。

      

    资源

    系列目录:WPF开发快速入门【0】前言与目录 

    代码下载:Learn WPF: WPF学习笔记 (gitee.com)

  • 相关阅读:
    指针相关面试题目
    VUE学习三:双向绑定指令(v-mode)、组件化开发(全局组件/局部组卷/组件通信)、组件化高级(slot插槽使用)
    MongoDB聚合运算符:$bottom
    C语言入门 Day_11 判断的嵌套
    Banana Pi开源社区开源硬件瑞芯微RK3568/RK3588全国产化支持计划
    分布式文件系统FastDFS
    Linux eBPF介绍(二)
    this is biaoti
    Python Web开发(十一):ORM 对关联表的操作
    现在的发票有发票专用章吗?如何验证发票真伪?百望云为您详解!
  • 原文地址:https://www.cnblogs.com/seabluescn/p/16615623.html