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<ListViewAdvNodeItem> 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;
        }

    }

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)

Tags: