写在前面:本文为即兴而作,因此难免有疏漏和词不达意的地方。在这里,非常期望您提供评论,分享您的想法和建议。
这是一篇介绍如何在WPF中实现拖放功能的短文。
首先要读者清楚的一件事情是:拖放主要分为拖放源和拖放目标两个组成。拖放源和拖放目标各自拥有不同的事件。软件开发人员需要在适当的事件中完成相应功能。
试想拖放是如何操作的:用户选中一个界面元素,并在鼠标左键按下的情况下移动鼠标,最后,在到达拖放目标时松开鼠标左键,从而完成数据拖放的全过程。从程序编写的角度来看,用户需要在左键选中项目并按下的情况下移动以启动拖放,并在鼠标移动的过程中给出当前拖放状态的外观回馈,并在松开鼠标时尝试将项目添加到目标中。
对于任意的软件界面,鼠标左键按下并移动的行为并不一定会导致拖放的开始。因此软件开发人员需要自行编写代码启动拖放功能,而不是由WPF决定。这也就是软件开发人员在特定条件下需要自行调用DragDrop.DoDragDrop()以启动拖放操作的原因。DragDrop.DoDragDrop()函数接受三个参数:dragSource、data以及allowedEffects。特别需要注意的是dragSource参数。该参数标示了拖拽操作的消息源,也决定了所有的消息源事件由谁发出。参数data则用来包装Drag&Drop所操作的数据。一般情况下,其都是一个DataObject类型的实例。该实例内部应包装拖拽所实际操作的数据。最后,allowedEffects可以用来指定拖拽操作的效果。调用该函数的片断可以如下所示:
1 DragDrop.DoDragDrop(mListBox, dataObject, DragDropEffects.Copy);
启动了拖拽操作以后,软件开发人员就需要处理拖拽过程中所发生的一系列事件了。在这种情况下,软件开发人员不能寄望于通过响应Mouse.MouseMove等事件完成拖拽行为的响应。这是因为DragDrop.DoDragDrop()函数实际上是一个阻塞函数。在拖拽行为终止之前,这些事件都不会被发送。取而代之的是,软件开发人员可以使用拖拽源和目标所提供的事件。拖拽源提供的事件为QueryContinueDrag、GiveFeedback以及对应的Preview-事件。QueryContinueDrag事件用来决定是否继续拖放操作。该事件发生的时机为键盘或鼠标按钮状态发生变化时。GiveFeedback则用来为用户提供拖放的反馈信息,如令被拖拽的界面元素的图像随鼠标变化,或提供一个Tooltip提示用户当前的拖放效果等。该消息会在拖拽过程中定时发出,因此软件开发人员也可以将其作为Timer事件使用。在这里强调一下,因为在拖拽过程中软件开发人员不能使用其它事件,因此了解各拖拽事件发生时机是一个很重要的事情。
另外,在您看到Preview-事件的时候,相信您一定能够想到这是一个隧道/冒泡路由事件。因此,对这些事件的处理并不一定需要向拖放源添加事件处理函数,而可以在较高层次中重写相应函数即可,如重写Window的OnGiveFeedback()函数。这样做的好处在于,其能提供较为集中的拖拽处理逻辑,并在需要更改较高层次界面元素,如窗口的状态栏状态时拥有较好的语义特征。这样做的不足之处也有,如其并不太适合窗口中拥有多个拖拽源的情况。当然,在高层次侦听消息源发出的消息也是非常好的选择。
如果我们需要实现在拖拽过程中将拖拽的界面元素的样子作为预览这一功能,那么该功能的实现如下所示:
1 mListBox.PreviewMouseMove += OnPreviewListBoxMouseMove;
2 mListBox.QueryContinueDrag += OnQueryContinueDrag;
3
4 private void OnQueryContinueDrag(object sender, QueryContinueDragEventArgs e)
5 {
6 mAdornerLayer.Update();
7 …
8 }
9
10 private void OnPreviewListBoxMouseMove(object sender, MouseEventArgs e)
11 {
12 ListBoxItem listBoxItem = … // Find your actual visual you want to drag
13 …
14 DragDropAdorner adorner = new DragDropAdorner(listBoxItem);
15 mAdornerLayer = AdornerLayer.GetAdornerLayer(mTopLevelGrid);
16 mAdornerLayer.Add(adorner);
17
18 DataItem dataItem = listBoxItem.Content as DataItem;
19 DataObject dataObject = new DataObject(dataItem.Clone());
20 // Here, we should notice that dragsource param will specify on which
21 // control the drag&drop event will be fired
22 System.Windows.DragDrop.DoDragDrop(mListBox, dataObject, DragDropEffects.Copy);
23 …
24 }
其中DragDropAdorner用来显示被拖拽的界面元素的预览。其使用了Adorner。如果读者对该控件的实现有兴趣,请自行下载示例程序查看。有关Adorner的使用,我会在有时间的时候专门撰写一篇文章。
接下来就是拖拽目标事件了。拖拽目标会发送DragEnter、DragOver、DragLeave、Drop以及相应的Preview-事件。这些事件的意义十分清晰,相信您从名字中就能看出这些事件发生的时机。在这些事件中需要注意的则是传入的DragEventArgs。通过设置它的Effects成员,软件开发人员可以控制鼠标的状态,以提示用户当前拖拽动作的光标反馈。同时通过它的Data属性,软件开发人员可以获得DoDragDrop()函数调用时所传入的数据。
下面就是一段响应拖拽目标事件的代码:
1 private void OnDragOver(object sender, DragEventArgs e)
2 {
3 e.Effects = DragDropEffects.None;
4
5 // Find the corresponding treeview item in mTreeView and select it
6 Point pos = e.GetPosition(mTreeView);
7 HitTestResult result = VisualTreeHelper.HitTest(mTreeView, pos);
8 if (result == null)
9 return;
10
11 TreeViewItem selectedItem = Utils.FindVisualParent<TreeViewItem>(result.VisualHit);
12 if (selectedItem != null)
13 selectedItem.IsSelected = true;
14
15 e.Effects = DragDropEffects.Copy;
16 }
17
18 private void OnDrop(object sender, DragEventArgs e)
19 {
20 // Drop the data item into corresponding treeview item
21 Point pos = e.GetPosition(mTreeView);
22 HitTestResult result = VisualTreeHelper.HitTest(mTreeView, pos);
23 if (result == null)
24 return;
25
26 TreeViewItem selectedItem = Utils.FindVisualParent<TreeViewItem>(result.VisualHit);
27 if (selectedItem == null)
28 return;
29
30 DataItem parent = selectedItem.Header as DataItem;
31 DataItem dataItem = e.Data.GetData(typeof(DataItem)) as DataItem;
32 if (parent != null && dataItem != null)
33 parent.Items.Add(dataItem);
34 }
需要读者注意的则是该段代码中对GetData()函数的调用。Drag&Drop过程中,如果希望从DataObject中获取数据,那么必须使用原有类型。如A是B的基类,而DataObject中封存的则是类型B的实例,那么软件开发人员需要在DataObject.GetData()中使用typeof(B),而不能是typeof(A)。
在实现拖拽功能的时候,软件开发人员需要注意一系列问题。
首先是DragDropEffect。该枚举中的每个值都对应着拖放过程的一种特定行为。这些外观在UI设计和用户使用中拥有特定的惯用法。因此在开发过程中要想好到底希望对拖拽目标执行何种操作,以防止用户在使用过程中产生疑惑。
另外,外部拖拽源也是一种常见的拖拽功能,如将文件拖拽到应用程序内以进行加载。在某些情况下,拖拽目标事件并不能提供所需的信息。如在拖拽多个文件到应用程序内的时候,IDataObject接口只提供了GetData()函数。在这种情况下,软件开发人员可以尝试将其转化为DataObject类型实例,并通过GetFileDropList()函数返回所有被拖拽的文件。
同时需要注意的是鼠标位置的获取方法。在拖拽过程中,鼠标的位置不能通过Mouse类等WPF标准方法获得。在某些情况下,该方法将会返回一个错误的位置。这是因为在拖拽过程中,鼠标的控制权是由拖拽源所管理的。该管理过程中会使用Win32函数,从而使WPF无法正确地返回鼠标的位置信息。一个变通的方法则是使用PInvoke调用Win32 API GetCursorPos()。
另一个需要提及的小技巧则是如何禁用拖拽。标准控件包括一些默认情况下可作为拖拽目标的控件,如TextBox。为了禁止该功能,软件开发人员可以将OnPreviewDragEnter()和OnPreviewDragOver()重载中DragEventArgs的Handled属性设置为true,并设置Effects为None,以模拟禁止拖放的效果。
源码下载:http://download.csdn.net/detail/silverfox715/3884722
注明 原文地址:http://www.cnblogs.com/loveis715/archive/2011/12/05/2277384.html