拖放官方文档
拖放操作通常涉及两个参与方:拖动对象所源自的拖动源和接收放置对象的拖放目标。 拖动源和放置目标可能是相同应用程序或不同应用程序中的 UI 元素。
我这里实现的是对TabControl的Tab页面进行拖放,以达成类似Chrome浏览器的拖放功能。
对拖放相关事件研究后发现,最终只需要在拖动源事件处理程序中调用 DoDragDrop 方法启动拖动操作,然后在拖放目标上,响应Drop事件。启动拖放操作的拖动源事件通常是 MouseMove事件,另外需要判断鼠标左键是否为按下状态。另外,最好判断一下鼠标移动的距离是否大于最小拖动距离(在我没有添加这个检查之前,双击也会触发拖动事件)。
除了正常拖动(将Tab页从一个TabControl 拖动到另一个 TabControl 中),考虑将Tab页拖动到窗口之外的情况(浏览器在这种情况下会创建一个新的窗口来显示这个Tab页),我这里采用类似的方式,创建一个预设的子窗口来显示该Tab页面。
此外,当Tab页面在原TabControl内部拖动时,根据拖动位置,调整Tab页面的顺序。
我这里对原生的TabControl进行了继承封装了几个响应事件,轻量化的实现了拖放效果。
TabControlDragable.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;namespace WpfLibrary;public class TabControlDragable : TabControl
{public TabControlDragable(){AddHandler(MouseLeftButtonDownEvent, new MouseButtonEventHandler(OnMouseLeftButtonDown), true);AddHandler(MouseLeftButtonUpEvent, new MouseButtonEventHandler(OnMouseLeftButtonUp), true);AddHandler(MouseMoveEvent, new MouseEventHandler(OnMouseMove), true);AddHandler(DropEvent, new DragEventHandler(OnDrop), true);MinWidth = 100;MinHeight = 100;AllowDrop = true;}private bool IsDraging = false;private object? DragData;private Point DragStartPosition;private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e){Point p = e.GetPosition(this);DragData = DragUtility.GetDataObjectFromItemsControl(this, p);if (DragData != null){DragStartPosition = p;}e.Handled = true;}private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e){ResetState();e.Handled = true;}private void OnMouseMove(object sender, MouseEventArgs e){if (DragData == null || DragData is not TabItem){return;}Point currentPosition = e.GetPosition(this);if (e.LeftButton == MouseButtonState.Pressed && !IsDraging&& ((Math.Abs(currentPosition.X - DragStartPosition.X) > SystemParameters.MinimumHorizontalDragDistance) || (Math.Abs(currentPosition.Y - DragStartPosition.Y) > SystemParameters.MinimumVerticalDragDistance))){DataObject dataObject = new DataObject();dataObject.SetData("DragData", DragData);var effects = DragDrop.DoDragDrop(this, dataObject, DragDropEffects.Move);if (effects == DragDropEffects.None){if (Items.Count != 1){var tabItem = DragData as TabItem;var parentWindow = DragUtility.GetParentWindow(tabItem);Items.Remove(tabItem);ChildWindow newChildWindow = new ChildWindow();if (parentWindow is ChildWindow){newChildWindow.Owner = parentWindow.Owner;}else{newChildWindow.Owner = parentWindow;}newChildWindow.DataContext = parentWindow?.DataContext;var tabControl = new TabControlDragable();newChildWindow.MyGrid.Children.Add(tabControl);tabControl.Items.Add(tabItem);newChildWindow.Show();}}ResetState();}e.Handled = true;}private int MeasureIndex(TabControl targetTabControl, Point point){double preWidth = 0.0;for (var i = 0; i < targetTabControl.Items.Count; i++){var item = targetTabControl.Items[i] as TabItem;if (item == null){continue;}if (point.Y > item.ActualHeight){return targetTabControl.Items.Count;}if (point.X < preWidth + item.ActualWidth / 2){return i;}else{preWidth += item.ActualWidth;}}return targetTabControl.Items.Count;}private void OnDrop(object sender, DragEventArgs e){var tabItem = e.Data.GetData("DragData") as TabItem;var parentTabControl = tabItem?.Parent as TabControl;if (tabItem == null || parentTabControl == null){return;}var point = e.GetPosition(this);int index = MeasureIndex(this, point);if (this != parentTabControl){var parentWindow = DragUtility.GetParentWindow(parentTabControl);if (parentTabControl.Items.Count == 1){var childWindow = parentWindow as ChildWindow;if (childWindow != null){parentTabControl.Items.Remove(tabItem);this.Items.Insert(index, tabItem);this.SelectedItem = tabItem;childWindow?.Close();}}else{parentTabControl.Items.Remove(tabItem);this.Items.Insert(index, tabItem);this.SelectedItem = tabItem;}}else if (index < parentTabControl.Items.IndexOf(tabItem)){parentTabControl.Items.Remove(tabItem);parentTabControl.Items.Insert(index, tabItem);parentTabControl.SelectedItem = tabItem;}else if (index > parentTabControl.Items.IndexOf(tabItem)){parentTabControl.Items.Remove(tabItem);parentTabControl.Items.Insert(index - 1, tabItem);parentTabControl.SelectedItem = tabItem;}e.Handled = true;}private void ResetState(){IsDraging = false;DragData = null;}
}
使用起来也很简单,只需要将TabControl 替换为 TabControlDragable 即可,事件已经绑定好了。
MainWindow.xaml
<Window x:Class="WpfLibraryTest.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:WpfLibraryTest"xmlns:wpfLibrary="clr-namespace:WpfLibrary;assembly=WpfLibrary"mc:Ignorable="d"Title="MainWindow" Height="450" Width="800"><Grid><Grid.RowDefinitions><RowDefinition Height="*"/><RowDefinition Height="*"/></Grid.RowDefinitions><wpfLibrary:TabControlDragable Grid.Row="0" x:Name="tabControl1"><TabItem Header="tabControl1Item1"><DataGrid ItemsSource="{Binding Path=Accounts}" IsReadOnly="True"></DataGrid></TabItem><TabItem Header="tabControl1Item2"><DataGrid ItemsSource="{Binding Path=Accounts2}" IsReadOnly="True"></DataGrid></TabItem><TabItem Header="tabControl1Item3"><DataGrid ItemsSource="{Binding Path=Accounts3}" IsReadOnly="True"></DataGrid></TabItem></wpfLibrary:TabControlDragable><wpfLibrary:TabControlDragable Grid.Row="1" x:Name="tabControl2"><TabItem Header="tabControl2Item1"><DataGrid ItemsSource="{Binding Path=Accounts}" IsReadOnly="True"></DataGrid></TabItem><TabItem Header="tabControl2Item2"><DataGrid ItemsSource="{Binding Path=Accounts2}" IsReadOnly="True"></DataGrid></TabItem><TabItem Header="tabControl2Item3"><DataGrid ItemsSource="{Binding Path=Accounts3}" IsReadOnly="True"></DataGrid></TabItem></wpfLibrary:TabControlDragable></Grid>
</Window>