WPF实现鼠标拖动控件并带有中间动效
一. 前提
要实现鼠标对控件的拖拽移动,首先必须知道下面几点:
-
WPF中的鼠标左键按下、鼠标移动事件,有时候通过XAML界面添加的时候并有没有作用,我们要通过触发事件的元素和要监听的路由事件绑定来进行手动触发;
-
如果在移动时候要持续修改控件的属性,我们通过改变RenderTransform来修改呈现,而不是直接修改控件本身的属性(会卡);
-
通过VisualBrush来填充Rectangle,来实现鼠标拖动控件所形成的影子;
-
通过创建一个带有目标依赖属性的Button的子类,来将有关数据放入Button的子类中;
-
并不需要通过 UIElement.CaptureMouse() 和 UIElement.ReleaseMouseCapture()来对鼠标进行捕获和释放;
-
屏蔽一些键盘热键导致鼠标抬起的消息失去的问题,如:Alt + Ctrl + A 截图等等的热键的影响;
二. 过程
这里以按钮的拖动,分析一下这个过程:
-
首先在点击按钮(鼠标左键按下),我们以按钮为原型创建一个 “影子” ;
-
在鼠标按住左键拖动的时候,实现对这个 “影子” 的拖动跟随效果;
-
最后,在放开鼠标(鼠标左边抬起)时,将原来的按钮的位置直接移动到抬起时的位置并去除跟随的 “影子”;
三. 代码
这边的代码进行了封装,如过要看没有封装的版本请见示例工程(下面可以下载)
- DragButton 类,继承自 Button 类
/// <summary> /// 拖拽按钮 /// </summary> public class DragButton : Button { //依赖属性 private static readonly DependencyProperty IsDragProperty = DependencyProperty.Register("IsDrag", typeof(Boolean), typeof(DragButton)); private static readonly DependencyProperty CurrentPosProperty = DependencyProperty.Register("CurrentPos", typeof(Point), typeof(DragButton)); private static readonly DependencyProperty ClickPosProperty = DependencyProperty.Register("ClickPos", typeof(Point), typeof(DragButton)); private static readonly DependencyProperty RectProperty = DependencyProperty.Register("Rect", typeof(Rectangle), typeof(DragButton)); /// <summary> /// 是否拖拽 /// </summary> public bool IsDrag { get { return (bool)this.GetValue(IsDragProperty); } set { this.SetValue(IsDragProperty, value); } } /// <summary> /// 按钮的定位位置 /// 按钮左上角的位置 /// </summary> public Point CurrentPos { get { //第一次获取如果是没有被初始化,那么吧按钮的坐标初始化过来 Point p = (Point)this.GetValue(CurrentPosProperty); if (p.X == 0 && p.Y == 0) { p.X = Canvas.GetLeft(this); p.Y = Canvas.GetTop(this); } return p; } set { this.SetValue(CurrentPosProperty, value); } } /// <summary> /// 当前鼠标点在按钮上的位置 /// </summary> public Point ClickPos { get { return (Point)this.GetValue(ClickPosProperty); } set { this.SetValue(ClickPosProperty, value); } } /// <summary> /// 虚拟出来的按钮的显示矩形 /// </summary> public Rectangle Rect { get { if (this.GetValue(RectProperty) == null) { //创建VisualBrush VisualBrush visualBrush = new VisualBrush(this); Rectangle rect = new Rectangle() { Width = this.ActualWidth, Height = this.ActualHeight, Fill = visualBrush, Name = "rect" }; //设置值 Canvas.SetLeft(rect, Canvas.GetLeft(this)); Canvas.SetTop(rect, Canvas.GetTop(this)); rect.RenderTransform = new TranslateTransform(0d, 0d); rect.Opacity = 0.6; this.SetValue(RectProperty, rect); } return (Rectangle)this.GetValue(RectProperty); } } }
- MainWindow的XAML的部分代码
<Window x:Class="Demo.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:Demo" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525" x:Name="mainWindow"> <Canvas x:Name="canvas" Background="Aqua" Margin="0,0,0,0"> <local:DragButton x:Name="btn" Canvas.Left="173" Canvas.Top="64" Width="80" Height="30" Content="拖拽"/> <local:DragButton x:Name="btn1" Canvas.Left="94" Canvas.Top="166" Width="80" Height="30" Content="拖拽"/> </Canvas> </Window>
- MainWindow的C#后台部分代码
/// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); //添加事件 this.btn.AddHandler(Canvas.MouseLeftButtonDownEvent, new MouseButtonEventHandler(this.MouseButtonLeftDown), true); this.btn1.AddHandler(Canvas.MouseLeftButtonDownEvent, new MouseButtonEventHandler(this.MouseButtonLeftDown), true); //防止一些热键的影响 this.AddHandler(Window.KeyDownEvent, new RoutedEventHandler(this.OtherKeyDownEvent), true); } /// <summary> /// 区域移动事件 /// </summary> private void Canvas_MouseMove(object sender, MouseEventArgs e) { DragButton dragButton = sender as DragButton; if (dragButton != null && dragButton.IsDrag) { Point offsetPoint = e.GetPosition(this.canvas); double xOffset = offsetPoint.X - dragButton.CurrentPos.X - dragButton.ClickPos.X; double yOffset = offsetPoint.Y - dragButton.CurrentPos.Y - dragButton.ClickPos.Y; TranslateTransform transform = (TranslateTransform)dragButton.Rect.RenderTransform; transform.X += xOffset; transform.Y += yOffset; dragButton.CurrentPos = new Point(offsetPoint.X - dragButton.ClickPos.X, offsetPoint.Y - dragButton.ClickPos.Y); } } /// <summary> /// 鼠标左键按下 /// </summary> private void MouseButtonLeftDown(object sender, MouseButtonEventArgs e) { DragButton dragButton = sender as DragButton; if (dragButton != null && !dragButton.IsDrag) { dragButton.ClickPos = e.GetPosition(dragButton); this.canvas.Children.Add(dragButton.Rect); dragButton.IsDrag = true; //注册事件 dragButton.AddHandler(Canvas.MouseMoveEvent, new MouseEventHandler(this.Canvas_MouseMove), true); dragButton.AddHandler(Canvas.MouseLeftButtonUpEvent, new MouseButtonEventHandler(this.CanvasButtonLeftUp), true); } } /// <summary> /// 区域鼠标左键抬起 /// </summary> private void CanvasButtonLeftUp(object sender, MouseButtonEventArgs e) { ReducingButton(sender); } /// <summary> /// 防止一些热键的影响 /// </summary> private void OtherKeyDownEvent(object sender, RoutedEventArgs e) { ReducingButton(sender); } /// <summary> /// 避免重复代码 /// </summary> private void ReducingButton(object sender) { DragButton dragButton = sender as DragButton; if (dragButton != null && dragButton.IsDrag) { Canvas.SetLeft(dragButton, dragButton.CurrentPos.X); Canvas.SetTop(dragButton, dragButton.CurrentPos.Y); this.canvas.Children.Remove(dragButton.Rect); dragButton.IsDrag = false; //移除事件 dragButton.RemoveHandler(Canvas.MouseMoveEvent, new MouseEventHandler(this.Canvas_MouseMove)); dragButton.RemoveHandler(Canvas.MouseLeftButtonUpEvent, new MouseButtonEventHandler(this.CanvasButtonLeftUp)); } } }
四. 原理图
- 鼠标拖动的距离 = offsetPoint - ( CurrentPos + ClickPos) = offsetPoint - CurrentPos - ClickPos
- 鼠标拖动之后按钮左上角的坐标位置(相对于Canvas):Current = offsetPoint - ClickPos
五. 运行效果
六. 工程代码
七. 一些补充
这点的内容是后来自己看之前的代码,觉得不好之后修改了一下,然后补充的。一共写了4各版本,每个版本都在之前的版本上进行了优化,最终的版本是名字后面有 "最终版" 的那一个。
这边稍微记录一下:
1. 关于路由事件的绑定,之前看书的时候,书上并没有写的特别明白。首先 "UIElement.AddHandler" 这边的 UIElement 将会是事件 xxxHandler 的 sender 对象,而这个事件究竟是谁触发路由传递过来的,要通过 e.Source 或者 e.OriginalSource 来获得。总而言之,要让哪个元素来处理,则指明 UIElement ;处理什么,通过一棵树上的指定路由事件来传递;
2. 设置元素到 Canvas 子类的左边距的时候,使用:
Canvas.SetLeft(UIElement,double);
而设置的时候使用:
Canvas.GetLeft(UIElement);
而不是通过下面的方式来设置/获取:
UIElement.SetValue(DependencyProperty,object); UIElement.GetValue(DependencyProperty);
注:上面的方法可以是可以,就是写的比较烦琐,我们要充分利用附加属性的特点。一般附加属性的设置都在附加属性所在的对象上而不是在被附加的对象上,例如给 Person 增加一个学校的 School 类的班级的附加属性,那么这个设置班级附加属性的方法应该存在于学校 School 中。所以这边和直接调用学校 School 的方法来给 Person 添加班级属性是一个道理。
3. 路由事件可以进行延迟绑定,不需要在开始的时候就进行声明;
4. 关于 UIElement.CaptureMouse() 和 UIElement.ReleaseMouseCapture() 是不是要让元素捕获鼠标,防止一些特殊 Bug ,这个要依据情况来定。这边,我们每当要用来鼠标点击、拖拽的时候,就要考虑到这个问题。
5. 对于 Canvas 等等的元素的填充,可以使用 Margin = "0,0,0,0" 来实现;