WPF开发快速入门【7】WPF的拖放功能(Drag and Drop)
阅读原文时间:2023年07月08日阅读:2

概述

本文描述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 x:Name="listView"  
          Mouse.MouseMove="listView\_MouseMove" >  
</ListView>

在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 x:Name="treeView"  
          AllowDrop="True"  
          DragDrop.Drop="treeView\_Drop"  
          DragDrop.DragOver="treeView\_DragOver"  
          DragDrop.DragEnter="treeView\_DragEnter"  
          DragDrop.DragLeave="treeView\_DragLeave" >  
</TreeView>

首先要设置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定义

    <TreeView.ItemTemplate>  
        <HierarchicalDataTemplate DataType="{x:Type local:TreeViewAdvNodeItem}" ItemsSource="{Binding Children}">  
            <Border x:Name="itemBorder" Margin="2" BorderBrush="White" BorderThickness="1" >  
                <StackPanel Orientation="Horizontal">  
                    <Image x:Name="nodeImage" Source="../Images/FolderClose.png" Width="20" Height="20"/>  
                    <TextBlock Text="{Binding Title}" Tag="{Binding Excerpt}" VerticalAlignment="Center" FontSize="13" Margin="2"/>  
                </StackPanel>  
            </Border>  
        </HierarchicalDataTemplate>  
    </TreeView.ItemTemplate>

从这个模板定义可以看出,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;  
    }

}

在构造这个对象时,我们将传入两个重要的参数: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)

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章