一. 前提
要實現鼠標對控件的拖拽移動,首先必須知道下面幾點:
-
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" 來實現;