今天結合之前做過的一些拖拽的例子來對這個方面進行一些總結,這里主要用兩個例子來說明在WPF中如何使用拖拽進行操作,元素拖拽是一個常見的操作,第一個拖拽的例子是將ListBox中的子元素拖拽到ListView的某一個節點,從而將該子元素作為當前節點的子節點。第二個例子就是將ListView的某一項拖拽到另外一項上從而使兩個子項位置互換,這兩個例子的原理類似,實現細節上有所差別,下面就具體分析一下這些細節。
DEMO1
一 示例截圖
圖一 示例一截圖
二 重點原理分析
2.1 前台代碼分析
這一部分主要是主界面的分析,主要包括兩個部分一個是左側的ListBox另外一個就是右側的TreeView,在Treeview上設置了兩個事件,一個是DragDrop
.DragOver事件,另外一個是DragDrop.Drop事件,同時設置TreeView的AllowDrop屬性為true,關於這兩個事件后面再做重點分析。首先看看前台代碼:
<Window x:Class="DragDrop.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:DragDrop" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <DataTemplate x:Key="listBoxTemplate" DataType="{x:Type local:DataItem}"> <TextBlock Text="{Binding Header}"/> </DataTemplate> <HierarchicalDataTemplate x:Key="treeViewTemplate" DataType="{x:Type local:DataItem}" ItemsSource="{Binding Items}"> <TextBlock Text="{Binding Header}"/> </HierarchicalDataTemplate> </Window.Resources> <Grid x:Name="mTopLevelGrid"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="10"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <ListBox x:Name="mListBox" Grid.Column="0" ItemsSource="{Binding Source={x:Static local:Data.Instance}, Path=ListBoxItems}" ItemTemplate="{StaticResource listBoxTemplate}"/> <TreeView x:Name="mTreeView" Grid.Column="2" ItemsSource="{Binding Source={x:Static local:Data.Instance}, Path=TreeViewItems}" ItemTemplate="{StaticResource treeViewTemplate}" AllowDrop="True" DragDrop.DragOver="OnDragOver" DragDrop.Drop="OnDrop"/> </Grid> </Window>
2.2 后台代碼分析
下面重點分析后台代碼,在構造函數中我們首先為ListBox訂閱了兩個事件,一個是:PreviewMouseMove,另外一個是QueryContinueDrag,關於第一個事件就不做過多的說明,第二個事件是QueryContinueDrag:QueryContinueDrag在MSDN上的解釋是在拖放操作期間鍵盤或鼠標按鈕的狀態改變時發生。這里在拖拽過程中由於鼠標的位置一直在移動,所以該函數會一直執行,那么我們來分析一下,這兩個函數中到底做了些什么事情。
private void OnPreviewListBoxMouseMove(object sender, MouseEventArgs e) { if (Mouse.LeftButton != MouseButtonState.Pressed) return; Point pos = e.GetPosition(mListBox); HitTestResult result = VisualTreeHelper.HitTest(mListBox, pos); if (result == null) return; ListBoxItem listBoxItem = Utils.FindVisualParent<ListBoxItem>(result.VisualHit); // Find your actual visual you want to drag if (listBoxItem == null || listBoxItem.Content != mListBox.SelectedItem || !(mListBox.SelectedItem is DataItem)) return; DragDropAdorner adorner = new DragDropAdorner(listBoxItem); mAdornerLayer = AdornerLayer.GetAdornerLayer(mTopLevelGrid); // Window class do not have AdornerLayer mAdornerLayer.Add(adorner); DataItem dataItem = listBoxItem.Content as DataItem; DataObject dataObject = new DataObject(dataItem.Clone()); System.Windows.DragDrop.DoDragDrop(mListBox, dataObject, DragDropEffects.Copy); mStartHoverTime = DateTime.MinValue; mHoveredItem = null; mAdornerLayer.Remove(adorner); mAdornerLayer = null; }
這段代碼是為ListBox訂閱的PreviewMouseMove事件,首先要獲取到將要拖拽的ListBoxItem對象,獲取到這個對象之后我們需要為mTopLevelGrid的AdornerLayer添加一個Adorner對象從而在拖拽的時候顯示當前對象。然后我們便啟動拖拽操作了 System.Windows.DragDrop.DoDragDrop(mListBox, dataObject, DragDropEffects.Copy); 只有啟動了拖拽操作,后面的TreeView才能夠執行相應的DragOver和Drop事件,在完成整個拖拽操作后才能夠執行這句代碼后面的釋放當前對象的一些操作,這個就是整個大體的流程。
在DragOver事件中我們只做了一件事就是當拖拽的對象移動到TreeViewItem的上面時,當前的TreeViewItem處於選中狀態,而在Drop事件中我們需要將拖拽的對象作為子元素添加到當前的TreeViewItem的下一級,這兩個過程最重要的都是通過VisualTreeHelper的HitTest(命中操作)來獲取到當前的TreeViewItem對象。這個也是非常重要的一個部分,下面貼出具體的代碼。
private void OnDragOver(object sender, DragEventArgs e) { e.Effects = DragDropEffects.None; Point pos = e.GetPosition(mTreeView); HitTestResult result = VisualTreeHelper.HitTest(mTreeView, pos); if (result == null) return; TreeViewItem selectedItem = Utils.FindVisualParent<TreeViewItem>(result.VisualHit); if (selectedItem != null) selectedItem.IsSelected = true; e.Effects = DragDropEffects.Copy; } private void OnDrop(object sender, DragEventArgs e) { Point pos = e.GetPosition(mTreeView); HitTestResult result = VisualTreeHelper.HitTest(mTreeView, pos); if (result == null) return; TreeViewItem selectedItem = Utils.FindVisualParent<TreeViewItem>(result.VisualHit); if (selectedItem == null) return; DataItem parent = selectedItem.Header as DataItem; DataItem dataItem = e.Data.GetData(typeof(DataItem)) as DataItem; if (parent != null && dataItem != null) parent.Items.Add(dataItem); }
另外一個重要的部分就是在拖拽的過程中我們需要不斷去更新當前Adorner更新位置,這里我們通過重寫OnRender函數來實現這一個目標。
public class DragDropAdorner : Adorner { public DragDropAdorner(UIElement parent) : base(parent) { IsHitTestVisible = false; // Seems Adorner is hit test visible? mDraggedElement = parent as FrameworkElement; } protected override void OnRender(DrawingContext drawingContext) { base.OnRender(drawingContext); if (mDraggedElement != null) { Win32.POINT screenPos = new Win32.POINT(); if (Win32.GetCursorPos(ref screenPos)) { Point pos =this.PointFromScreen(new Point(screenPos.X, screenPos.Y)); Rect rect = new Rect(pos.X, pos.Y, mDraggedElement.ActualWidth, mDraggedElement.ActualHeight); drawingContext.PushOpacity(1.0); Brush highlight = mDraggedElement.TryFindResource(SystemColors.HighlightBrushKey) as Brush; if (highlight != null) drawingContext.DrawRectangle(highlight, new Pen(Brushes.Transparent, 0), rect); drawingContext.DrawRectangle(new VisualBrush(mDraggedElement), new Pen(Brushes.Transparent, 0), rect); } } } FrameworkElement mDraggedElement = null; }
另外一點需要注意的是在ListBox訂閱的OnQueryContinueDrag事件中必須不停執行刷新的操作,否則當前的拖拽對象是不能夠實時進行更新操作的,這一點非常重要。
private void OnQueryContinueDrag(object sender, QueryContinueDragEventArgs e) { mAdornerLayer.Update(); UpdateTreeViewExpandingState(); }
這個示例就介紹到這里,關鍵是對整體的拖拽有一個概念性的理解,接着我會介紹下面的示例,介紹完畢之后會對兩者之間進行一個對比,對比之后會進一步對整個拖拽過程進行總結。
DEMO2
這一部分是一個ListView里面的Item的拖拽操作,通過拖拽操作能夠改變元素的位置,從而實現自定義排序的結果。
一 示例截圖
圖二 示例二截圖
二 重點原理分析
2.1 前台代碼分析
這一部分的前台代碼比較簡單就是在ListView中嵌套GridView對象,然后為當前對象綁定數據源,這里比較簡單不再贅述。
<Window x:Class="ListViewDragDemo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:ListViewDragDemo" xmlns:util="clr-namespace:ListViewDragDemo.Utils" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <Style x:Key="ItemContStyle" TargetType="ListViewItem"> <Style.Resources> <LinearGradientBrush x:Key="MouseOverBrush" StartPoint="0.5, 0" EndPoint="0.5, 1"> <GradientStop Color="#22000000" Offset="0" /> <GradientStop Color="#44000000" Offset="0.4" /> <GradientStop Color="#55000000" Offset="0.6" /> <GradientStop Color="#33000000" Offset="0.9" /> <GradientStop Color="#22000000" Offset="1" /> </LinearGradientBrush> </Style.Resources> <Setter Property="Padding" Value="0,8" /> <Setter Property="HorizontalContentAlignment" Value="Stretch" /> <Setter Property="Border.BorderThickness" Value="0,0,0,0.5" /> <Setter Property="Border.BorderBrush" Value="LightGray" /> <Style.Triggers> <Trigger Property="util:ListViewItemDragState.IsBeingDragged" Value="True"> <Setter Property="FontWeight" Value="DemiBold" /> </Trigger> <Trigger Property="util:ListViewItemDragState.IsUnderDragCursor" Value="True"> <Setter Property="Background" Value="{StaticResource MouseOverBrush}" /> </Trigger> </Style.Triggers> </Style> </Window.Resources> <Grid> <Border Background="#eee" Padding="2" Margin="0 5 5 5"> <DockPanel> <ListView x:Name="ListViewCtl" ItemsSource="{Binding Students}" ItemContainerStyle="{StaticResource ItemContStyle}" SelectionMode="Single"> <ListView.View> <GridView> <GridViewColumn Header="姓名" DisplayMemberBinding="{Binding Name}" Width="100"/> <GridViewColumn Header="性別" DisplayMemberBinding="{Binding Sex}" Width="100"/> <GridViewColumn Header="年級" DisplayMemberBinding="{Binding Grade}" Width="100" /> <GridViewColumn Header="分數" DisplayMemberBinding="{Binding Score}" Width="100"/> </GridView> </ListView.View> </ListView> </DockPanel> </Border> </Grid> </Window>
2.1 后台代碼分析
這一部分涉及到的內容比較多,所采用的方法也有所不同,需要進行比較,然后深化對知識的理解。
這一個示例與之前的示例的不同之處主要在於:1 不再使用QueryContinueDrag事件來更新當前的Adorner對象的位置,而在DragOver和DragEnter事件中去更新Adorner的位置。 2 獲取當前ListViewItem的方式不再使用VisualTreeHelper.HitTest方法來進行獲取。 3 Adorner對象中采用不同的重載方法來實現位置的更新操作。下面就這些區別來一一進行說明。
2.1.1 為當前的ListView訂閱事件
public ListView ListView { get { return listView; } set { if (this.IsDragInProgress) throw new InvalidOperationException("Cannot set the ListView property during a drag operation."); if (this.listView != null) { #region Unhook Events this.listView.PreviewMouseLeftButtonDown -= listView_PreviewMouseLeftButtonDown; this.listView.PreviewMouseMove -= listView_PreviewMouseMove; this.listView.DragOver -= listView_DragOver; this.listView.DragLeave -= listView_DragLeave; this.listView.DragEnter -= listView_DragEnter; this.listView.Drop -= listView_Drop; #endregion // Unhook Events } this.listView = value; if (this.listView != null) { if (!this.listView.AllowDrop) this.listView.AllowDrop = true; #region Hook Events this.listView.PreviewMouseLeftButtonDown += listView_PreviewMouseLeftButtonDown; this.listView.PreviewMouseMove += listView_PreviewMouseMove; this.listView.DragOver += listView_DragOver; this.listView.DragLeave += listView_DragLeave; this.listView.DragEnter += listView_DragEnter; this.listView.Drop += listView_Drop; #endregion // Hook Events } } }
注意在訂閱這些方法之前先取消訂閱,避免重復進行事件的訂閱,這個是非常好的一個習慣。
2.1.2 具體事件分析
首先我們看一看PreviewMouseLeftButtonDown這個事件,這個主要是進行一些初始化的操作。首先要獲取到當前鼠標位置處的ListViewItem的序號Index,采取的方法時查看當前鼠標的位置是否在ListView的某一個Item的范圍之內,這里使用了VisualTreeHelper.GetDescendantBounds這個方法來獲取元素的邊界位置然后再判斷當前鼠標位置是否位於此邊界之內,通過這種方式來獲取當前拖動的Index值。
void listView_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (this.IsMouseOverScrollbar) { //Set the flag to false when cursor is over scrollbar. this.canInitiateDrag = false; return; } int index = this.IndexUnderDragCursor; Debug.WriteLine(string.Format("this.IndexUnderDragCursor:{0}", index.ToString())); this.canInitiateDrag = index > -1; if (this.canInitiateDrag) { // Remember the location and index of the ListViewItem the user clicked on for later. this.ptMouseDown = MouseUtilities.GetMousePosition(this.listView); this.indexToSelect = index; } else { this.ptMouseDown = new Point(-10000, -10000); this.indexToSelect = -1; } } int IndexUnderDragCursor { get { int index = -1; for (int i = 0; i < this.listView.Items.Count; ++i) { ListViewItem item = this.GetListViewItem(i); if (this.IsMouseOver(item)) { index = i; break; } } return index; } } bool IsMouseOver(Visual target) { // We need to use MouseUtilities to figure out the cursor // coordinates because, during a drag-drop operation, the WPF // mechanisms for getting the coordinates behave strangely. Rect bounds = VisualTreeHelper.GetDescendantBounds(target); Point mousePos = MouseUtilities.GetMousePosition(target); return bounds.Contains(mousePos); }
下一步就是在PreviewMouseMove事件中獲取ListViewItem,添加Adorner,然后再啟動拖放操作,這個過程和上面的示例中一樣,初始化AdornerLayer的過程在InitializeAdornerLayer這個函數中進行,主要是在當前的ListView的的AdornerLayer層添加拖拽的Adorner對象。
AdornerLayer InitializeAdornerLayer(ListViewItem itemToDrag) { // Create a brush which will paint the ListViewItem onto // a visual in the adorner layer. VisualBrush brush = new VisualBrush(itemToDrag); // Create an element which displays the source item while it is dragged. this.dragAdorner = new DragAdorner(this.listView, itemToDrag.RenderSize, brush); // Set the drag adorner's opacity. this.dragAdorner.Opacity = this.DragAdornerOpacity; AdornerLayer layer = AdornerLayer.GetAdornerLayer(this.listView); layer.Add(dragAdorner); // Save the location of the cursor when the left mouse button was pressed. this.ptMouseDown = MouseUtilities.GetMousePosition(this.listView); return layer; }
這里面的重點是DragAdorner這個類,這個當前的拖拽的對象,我們也就此分析一下。
public class DragAdorner : Adorner { #region Data private Rectangle child = null; private double offsetLeft = 0; private double offsetTop = 0; #endregion // Data #region Constructor /// <summary> /// Initializes a new instance of DragVisualAdorner. /// </summary> /// <param name="adornedElement">The element being adorned.</param> /// <param name="size">The size of the adorner.</param> /// <param name="brush">A brush to with which to paint the adorner.</param> public DragAdorner(UIElement adornedElement, Size size, Brush brush) : base(adornedElement) { Rectangle rect = new Rectangle(); rect.Fill = brush; rect.Width = size.Width; rect.Height = size.Height; rect.IsHitTestVisible = false; this.child = rect; } #endregion // Constructor #region Public Interface #region GetDesiredTransform /// <summary> /// Override. /// </summary> /// <param name="transform"></param> /// <returns></returns> public override GeneralTransform GetDesiredTransform(GeneralTransform transform) { GeneralTransformGroup result = new GeneralTransformGroup(); result.Children.Add(base.GetDesiredTransform(transform)); result.Children.Add(new TranslateTransform(this.offsetLeft, this.offsetTop)); return result; } #endregion // GetDesiredTransform #region OffsetLeft /// <summary> /// Gets/sets the horizontal offset of the adorner. /// </summary> public double OffsetLeft { get { return this.offsetLeft; } set { this.offsetLeft = value; UpdateLocation(); } } #endregion // OffsetLeft #region SetOffsets /// <summary> /// Updates the location of the adorner in one atomic operation. /// </summary> /// <param name="left"></param> /// <param name="top"></param> public void SetOffsets(double left, double top) { this.offsetLeft = left; this.offsetTop = top; this.UpdateLocation(); } #endregion // SetOffsets #region OffsetTop /// <summary> /// Gets/sets the vertical offset of the adorner. /// </summary> public double OffsetTop { get { return this.offsetTop; } set { this.offsetTop = value; UpdateLocation(); } } #endregion // OffsetTop #endregion // Public Interface #region Protected Overrides /// <summary> /// Override. /// </summary> /// <param name="constraint"></param> /// <returns></returns> protected override Size MeasureOverride(Size constraint) { this.child.Measure(constraint); return this.child.DesiredSize; } /// <summary> /// Override. /// </summary> /// <param name="finalSize"></param> /// <returns></returns> protected override Size ArrangeOverride(Size finalSize) { this.child.Arrange(new Rect(finalSize)); return finalSize; } /// <summary> /// Override. /// </summary> /// <param name="index"></param> /// <returns></returns> protected override Visual GetVisualChild(int index) { return this.child; } /// <summary> /// Override. Always returns 1. /// </summary> protected override int VisualChildrenCount { get { return 1; } } #endregion // Protected Overrides #region Private Helpers private void UpdateLocation() { AdornerLayer adornerLayer = this.Parent as AdornerLayer; if (adornerLayer != null) adornerLayer.Update(this.AdornedElement); } #endregion // Private Helpers }
在這里面我們不再是重寫基類的OnRender這個函數而是采用重寫GetDesiredTransform來實現當前的DragAdorner位置隨着鼠標的變化而變化的,這里需要注意的是要重寫基類的GetVisualChild(int index)這個方法和VisualChildrenCount這個屬性,否則程序會無法執行。在解釋了這一部分之后就重點來講述DragEnter、DragOver、DragLeave和Drop這幾個函數了,這幾個是整個拖拽過程的重點。
首先是DragEnter,這個當鼠標移入進去的時候會觸發此事件,主要是來更新當前DragAdorner的顯示狀態,DragLeave事件是當當前拖拽的對象移出整個ListView的時候會使DragAdorner不顯示。DragOver是一個比較重要的事件,主要是進行兩方面的工作,一個是在拖拽時更新當前Adorner的位置,另外就是獲取新的鼠標位置處的ListViewItem對象,這個是最后執行Drop事件的最重要的准備工作,有了這些操作,那么最后一步就是調換拖拽的源和目標處的ListViewItem,從而最終完成拖拽操作。
其實對比兩個示例的過程,很多地方都有相似的地方,關鍵是把整個過程弄清楚,這樣無論是從一個控件拖拽到另外一個控件上還是從一個控件的一個項拖到這個控件的另外一個項上都可以實現。
最后請點擊此處獲得整個工程的源碼!