【问题标题】:Porting WinForms drag and drop to WPF drag and drop将 WinForms 拖放移植到 WPF 拖放
【发布时间】:2013-07-16 04:14:22
【问题描述】:

我正在将我的程序从 WinForms 移植到 WPF,并且在拖放时遇到了一些问题。它应该允许从 TreeView(它就像一个文件浏览器)拖动到打开文件的文本框。但是,WPF 版本的行为类似于自动复制和粘贴TreeViewItem 的标题文本。我想我只是有一些混淆?可能是DataObject 的东西。

功能齐全的相关 WinForms 代码:

private void treeView1_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button != MouseButtons.Left) return;
    TreeNode node = treeView1.GetNodeAt(e.Location);
    if (node != null) treeView1.DoDragDrop(node, DragDropEffects.Move);
}

textbox[i].DragDrop += (o, ee) =>
{
     if (ee.Data.GetDataPresent(typeof(TreeNode)))
     {
         TreeNode node = (TreeNode)ee.Data.GetData(typeof(TreeNode));   
         ((Textbox)o).Text = File.ReadAllLines(pathRoot + node.Parent.FullPath);
         ...

应该做同样事情的 WPF 代码:

private void TreeView_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    TreeViewItem item = e.Source as TreeViewItem;
    if (item != null)
    {
        DataObject dataObject = new DataObject();
        dataObject.SetData(DataFormats.StringFormat, GetFullPath(item));
        DragDrop.DoDragDrop(item, dataObject, DragDropEffects.Move);
    }
}

//textbox[i].PreviewDrop += textbox_Drop;
private void textbox_Drop(object sender, DragEventArgs e)
{
    TreeViewItem node = (TreeViewItem)e.Data.GetData(typeof(TreeViewItem)); //null?
    ((Textbox)sender).Text = ""; 
    //this is being executed BUT then the node's header text is being pasted
    //also, how do I access the DataObject I passed?
}

问题:在我的 WPF 版本中,我将文本框的文本设置为空(作为测试),这会发生,但随后粘贴了 TreeViewItem 的标题文本,这不是我想要的。

问题:将此 WinForms 代码移植到 WPF 的正确方法是什么?为什么在 WPF 版本中粘贴文本?我该如何防止呢?我使用了正确的事件吗?如何访问textbox_Drop 中的DataObject,以便可以像在WinForms 版本中一样打开文件?为什么 WPF 版本中 TreeViewItem 节点总是为空?

【问题讨论】:

标签: c# .net wpf winforms


【解决方案1】:

啊,到底是什么,我将我的评论扩展为一个答案:

如前所述,阅读链接如下: http://msdn.microsoft.com/en-us/library/hh144798.aspx

简而言之,TextBox 派生的控件已经实现了基本拖放操作的大部分“胆量”,建议您扩展它而不是提供显式的 DragEnter/DragOver/Drop 处理程序。

假设树“数据”结构如下:

public class TreeThing
{
   public string Description { get; set; }
   public string Path { get; set; }
}

处理程序可能看起来像这样:

this.tb.AddHandler(UIElement.DragOverEvent, new DragEventHandler((sender, e) =>
    {
        e.Effects = !e.Data.GetDataPresent("treeThing") ? 
            DragDropEffects.None : 
            DragDropEffects.Copy;                    
    }), true);

this.tb.AddHandler(UIElement.DropEvent, new DragEventHandler((sender, e) =>
{
    if (e.Data.GetDataPresent("treeThing"))
    {
        var item = e.Data.GetData("treeThing") as TreeThing;
        if (item != null)
        {
            tb.Text = item.Path;
            // TODO: Actually open up the file here
        }
    }
}), true);

只是为了搞笑,这是一个快速而肮脏的测试应用程序,它纯粹是在炫耀它使用响应式扩展 (Rx) 来进行拖动开始的东西:

XAML:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <TreeView x:Name="tree" Grid.Column="0" ItemsSource="{Binding TreeStuff}" DisplayMemberPath="Description"/>
        <TextBox x:Name="tb" Grid.Column="1" AllowDrop="True" Text="Drop here" Height="30"/>
    </Grid>
</Window>

讨厌的代码隐藏(懒得 MVVM 这个):

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Reactive.Linq;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace WpfApplication1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public MainWindow()
        {
            InitializeComponent();
            TreeStuff = new ObservableCollection<TreeThing>()
                {
                    new TreeThing() { Description="file 1",  Path = @"c:\temp\test.txt" },
                    new TreeThing() { Description="file 2", Path = @"c:\temp\test2.txt" },
                    new TreeThing() { Description="file 3", Path = @"c:\temp\test3.txt" },
                };

            var dragStart = 
                from mouseDown in 
                    Observable.FromEventPattern<MouseButtonEventHandler, MouseEventArgs>(
                        h => tree.PreviewMouseDown += h, 
                        h => tree.PreviewMouseDown -= h)
                let startPosition = mouseDown.EventArgs.GetPosition(null)
                from mouseMove in 
                    Observable.FromEventPattern<MouseEventHandler, MouseEventArgs>(
                        h => tree.MouseMove += h, 
                        h => tree.MouseMove -= h)
                let mousePosition = mouseMove.EventArgs.GetPosition(null)
                let dragDiff = startPosition - mousePosition
                where mouseMove.EventArgs.LeftButton == MouseButtonState.Pressed && 
                    (Math.Abs(dragDiff.X) > SystemParameters.MinimumHorizontalDragDistance ||
                    Math.Abs(dragDiff.Y) > SystemParameters.MinimumVerticalDragDistance)
                select mouseMove;

            dragStart.ObserveOnDispatcher().Subscribe(start =>
                {
                    var nodeSource = this.FindAncestor<TreeViewItem>(
                        (DependencyObject)start.EventArgs.OriginalSource);
                    var source = start.Sender as TreeView;
                    if (nodeSource == null || source == null)
                    {
                        return;
                    }
                    var data = (TreeThing)source
                        .ItemContainerGenerator
                        .ItemFromContainer(nodeSource);
                    DragDrop.DoDragDrop(nodeSource, new DataObject("treeThing", data), DragDropEffects.All);
                });

            this.tb.AddHandler(UIElement.DragOverEvent, new DragEventHandler((sender, e) =>
                {
                    e.Effects = !e.Data.GetDataPresent("treeThing") ? 
                        DragDropEffects.None : 
                        DragDropEffects.Copy;                    
                }), true);

            this.tb.AddHandler(UIElement.DropEvent, new DragEventHandler((sender, e) =>
            {
                if (e.Data.GetDataPresent("treeThing"))
                {
                    var item = e.Data.GetData("treeThing") as TreeThing;
                    if (item != null)
                    {
                        tb.Text = item.Path;
                       // TODO: Actually open up the file here
                    }
                }
            }), true);
            this.DataContext = this;
        }


        private T FindAncestor<T>(DependencyObject current)
            where T:DependencyObject
        {
            do
            {
                if (current is T)
                {
                    return (T)current;
                }
                current = VisualTreeHelper.GetParent(current);
            }
            while (current != null);
            return null;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public ObservableCollection<TreeThing> TreeStuff { get; set; }

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }

    public class TreeThing
    {
        public string Description { get; set; }
        public string Path { get; set; }
    }
}

【讨论】:

    【解决方案2】:

    你有不止一个问题,足以让这变得困难。第一个问题是您的拖动对象错误,您正在拖动一个字符串但仍在检查 TreeViewItem。只需使用与 Winforms 中相同的方法,拖动节点即可。第二个问题是 TextBox 已经实现了 D+D 支持,这妨碍了您的代码。以及您看到文字在放置后出现的原因。

    让我们首先处理拖动的开始。您需要做一些额外的工作,因为您开始拖动的方式会干扰 TreeView 的正常使用,因此很难选择一个节点。仅当鼠标移动足够远时才开始拖动:

        private Point MouseDownPos;
    
        private void treeView1_PreviewMouseDown(object sender, MouseButtonEventArgs e) {
            MouseDownPos = e.GetPosition(treeView1);
        }
    
        private void treeView1_PreviewMouseMove(object sender, MouseEventArgs e) {
            if (e.LeftButton == MouseButtonState.Released) return;
            var pos = e.GetPosition(treeView1);
            if (Math.Abs(pos.X - MouseDownPos.X) >= SystemParameters.MinimumHorizontalDragDistance ||
                Math.Abs(pos.Y - MouseDownPos.Y) >= SystemParameters.MinimumVerticalDragDistance) {
                TreeViewItem item = e.Source as TreeViewItem;
                if (item != null) DragDrop.DoDragDrop(item, item, DragDropEffects.Copy);
            }
        }
    

    现在是 drop,您需要实现 DragEnter、DragOver 和 Drop 事件处理程序,以避免 TextBox 中内置的默认 D+D 支持妨碍您。将 e.Handled 属性设置为 true 是必要的:

        private void textBox1_PreviewDragEnter(object sender, DragEventArgs e) {
            if (e.Data.GetDataPresent(typeof(TreeViewItem))) e.Effects = e.AllowedEffects;
            e.Handled = true;
        }
    
        private void textBox1_PreviewDrop(object sender, DragEventArgs e) {
            var item = (TreeViewItem)e.Data.GetData(typeof(TreeViewItem));
            textBox1.Text = item.Header.ToString();   // Replace this with your own code
            e.Handled = true;
        }
    
        private void textBox1_PreviewDragOver(object sender, DragEventArgs e) {
            e.Handled = true;
        }
    

    【讨论】:

      【解决方案3】:

      问题:在我的 WPF 版本中,我将文本框的文本设置为空(作为测试),这发生了,但之后粘贴了 TreeViewItem 的标题文本,这不是我想要的。

      我认为父 UI 元素正在处理(并因此覆盖)Drop 事件,因此您无法获得预期的结果。事实上,在尝试重新创建您的问题时,我什至无法触发 TextBox.Drop 事件。但是,使用 TextBox 的 PreviewDrop 事件,我能够得到(我认为)您的预期结果。试试这个:

          private void textBox1_PreviewDrop(object sender, DragEventArgs e)
          {
              TextBox tb = sender as TextBox;
              if (tb != null)
              {
                  // If the DataObject contains string data, extract it.
                  if (e.Data.GetDataPresent(DataFormats.StringFormat))
                  {
                      string fileName = e.Data.GetData(DataFormats.StringFormat) as string;
                      using (StreamReader s = File.OpenText(fileName))
                      {
                          ((TextBox)sender).Text = s.ReadToEnd();
                      }
                  }
              }
              e.Handled = true; //be sure to set this to true
          }
      

      我认为代码 sn-p 应该回答您提出的大多数问题,除了这个:

      为什么WPF版本中TreeViewItem节点总是为空?

      您在 DragDrop 事件中传递的 DataObject 不支持传递 TreeViewItem。在您(和我的)代码中,我们指定数据格式为DataFormats.StringFormat,不能转换为TreeViewItem

      【讨论】:

      • 谢谢,我需要e.Handled = true 并修复我检查e.Data 的方式。
      【解决方案4】:

      GetFullPath 似乎输出了错误的值。您要拖放的是Header,您可以直接从item 获取它。另外请记住,下面的方法与TreeViewMouseMove Event 相关联。

      private void TreeView_MouseMove(object sender, MouseButtonEventArgs e)
      {
          if (e.LeftButton != MouseButtonState.Pressed) return;
          TreeViewItem item = e.Source as TreeViewItem;
          if (item != null)
          {
              DataObject dataObject = new DataObject();
              dataObject.SetData(DataFormats.StringFormat, item.Header);
              DragDrop.DoDragDrop(item, dataObject, DragDropEffects.Move);
          }
      }
      

      我确实根据文本而不是 TreeViewItem (e.Data.GetData(typeof(string)).ToString()) 创建了放置部分,但最令人惊讶的是它甚至不是必需的。如果您打开一个新的 C# WPF 项目,在其上放置一个 TreeView 和一个 TextBox(更新 XAML 部分)并复制上面的代码,您可以将文本从 TreeView 拖放到 TextBox 而无需执行还要别的吗!!文本被复制到TextBox,而不考虑Drop handling

      【讨论】:

        【解决方案5】:

        我是否使用了正确的事件?: 我认为您使用的是正确的事件,但我认为您的代码中有几个问题。 我假设您已将树视图的 DataContext 设置为真实项目并使用绑定。

        1. 如何访问 textbox_Drop 中的 DataObject ? --> 要获取 DataObject,您必须通过递归获取真实项目(可能的其他解决方案)

          DependencyObject k = VisualTreeHelper.HitTest(tv_treeView, DagEventArgs.GetPosition(lv_treeView)).VisualHit;
          
          while (k != null)
              {
                  if (k is TreeViewItem)
                  {
                      TreeViewItem treeNode = k as TreeViewItem;
          
                      // Check if the context is your desired type
                      if (treeNode.DataContext is YourType)
                      {
                          // save the item
                          targetTreeViewItem = treeNode;
          
                          return;
                      }
                  }
                  else if (k == tv_treeview)
                  {
                      Console.WriteLine("Found treeview instance");
                      return;
                  }
          
                  // Get the parent item if no item from YourType was found
                  k = VisualTreeHelper.GetParent(k);
              }
          
        2. 为什么在 WPF 版本中粘贴文本? --> 显示标头是因为(我假设)它就像您的项目上的 tostring 方法。如果未指定复杂项目的绑定,则执行 ToString 方法。 尽量不要在 drop 事件的处理程序中直接设置 Text。将数据上下文设置为您的项目(到您在 1. 中找到的项目),然后通过 XAML 指定绑定路径。 (用于展示)

        【讨论】:

        • 这不能回答我的问题,您复制和粘贴的代码也没有帮助。
        • 我在回答中添加了您的问题。
        • 为什么我需要得到父母?我看不出代码是如何相关的,你能把它应用到我的代码中吗?正如您在我的工作 WinForms 代码中所见,我正在手动设置文本以进行测试。我应该可以让它在 WPF 中同样工作。
        • 依赖对象可以是 WPF 中的层次结构元素,比如一些布局或容器或文本框。取决于您为项目指定的模板。使用上面的算法,您将在这个层次结构中走到所有布局内容背后的原始项目。使用此项目,您可以通过 Datacontext 属性输入真实对象。因此,您需要上面的这段代码。
        • 我还是没明白你的意思。你能把它应用到我的代码中吗?我想要的只是与 TreeViewItem 关联的字符串,以便我可以打开文件。
        猜你喜欢
        • 2017-08-06
        • 2010-10-20
        • 2010-11-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-08-05
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多