WPF编程,实现鼠标拖动控件并带有中间动效

一. 前提

要实现鼠标对控件的拖拽移动,首先必须知道下面几点:

  1. WPF中的鼠标左键按下、鼠标移动事件,有时候通过XAML界面添加的时候并有没有作用,我们要通过触发事件的元素和要监听的路由事件绑定来进行手动触发;

  2. 如果在移动时候要持续修改控件的属性,我们通过改变RenderTransform来修改呈现,而不是直接修改控件本身的属性(会卡);

  3. 通过VisualBrush来填充Rectangle,来实现鼠标拖动控件所形成的影子;

  4. 通过创建一个带有目标依赖属性的Button的子类,来将有关数据放入Button的子类中;

  5. 并不需要通过 UIElement.CaptureMouse() 和 UIElement.ReleaseMouseCapture()来对鼠标进行捕获和释放;

  6. 屏蔽一些键盘热键导致鼠标抬起的消息失去的问题,如:Alt + Ctrl + A 截图等等的热键的影响;

二. 过程

这里以按钮的拖动,分析一下这个过程:

  1. 首先在点击按钮(鼠标左键按下),我们以按钮为原型创建一个 “影子” ;

  2. 在鼠标按住左键拖动的时候,实现对这个 “影子” 的拖动跟随效果;

  3. 最后,在放开鼠标(鼠标左边抬起)时,将原来的按钮的位置直接移动到抬起时的位置并去除跟随的 “影子”;

三. 代码

这边的代码进行了封装,如过要看没有封装的版本请见示例工程(下面可以下载)

  • 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” 来实现;

原文链接:https://www.cnblogs.com/Jeffrey-Chou/p/12249596.html


效果

在这里插入图片描述

另一种实现

布局

<Grid Grid.Row="0" Name="gridChart" Margin="50,20" SizeChanged="gridChart_SizeChanged">
    <Canvas Name="backCanvas" Background="Red">
        <Canvas Name="foreCanvas" Background="AliceBlue"/>
    </Canvas>
</Grid>

1、gridChart 是绘图区域,设置Margin是为了给坐标值腾出位置;SizeChanged事件,更新图
2、为了方便管理,用两个Canvas绘图
3、backCanvas 背景画板用于画一些辅助性的东西。如下图的网格线,背景颜色,坐标值0、50、100那些
4、foreCanvas 前景画蓝色方块,以及包裹它的四条直线,和四个坐标值
5、拖动效果通过方块的三个事件完成:MouseLeftButtonDown、MouseMove、MouseLeftButtonUp
在这里插入图片描述

逻辑代码

1、gridChart_SizeChanged

当窗口大小改变时,获取gridChart 的大小,给Canvas赋值。删除前景画板中的所有东西,背景画板留下一个,就是前景画板。
DrawGridLine:在背景画板中画网格线,参数是网格线的横线和纵向偏移量
DrawRec:在前景画板中画蓝色方块,参数是方块的长宽

private void gridChart_SizeChanged(object sender, SizeChangedEventArgs e)
{
    backCanvas.Width = gridChart.ActualWidth;
    backCanvas.Height = gridChart.ActualHeight;
    foreCanvas.Width = gridChart.ActualWidth;
    foreCanvas.Height = gridChart.ActualHeight;

    backCanvas.Children.RemoveRange(1, backCanvas.Children.Count - 1);
    foreCanvas.Children.Clear();

    DrawGridLine(50, 50);
    DrawRec(100, 100);
}

2、DrawGridLine(double dx, double dy)

使用Line依次画垂直的网格和用Label画的横坐标。
使用Line依次画水平的网格和用Label画的纵坐标。

private void DrawGridLine(double dx, double dy)
{
    // 背景边框
    Border border = new Border()
    {
        Width = backCanvas.Width,
        Height = backCanvas.Height,
        BorderBrush = new SolidColorBrush(Color.FromRgb(0, 24, 113)),
        BorderThickness = new Thickness(1)
    };
    backCanvas.Children.Add(border);
    // 网格
    Line line = new Line();
    // 坐标值
    Label label = new Label();
    // 添加垂直网格
    double gridX = 0;
    double gridY = 0;
    while (gridX < backCanvas.Width)
    {
        line = new Line()
        {
            StrokeThickness = 0.1,
            Stroke = Brushes.Black,
            X1 = gridX,
            Y1 = 0,
            X2 = gridX,
            Y2 = backCanvas.Height
        };
        backCanvas.Children.Add(line);

        // 添加横坐标标签
        label = new Label();
        label.Content = gridX;
        backCanvas.Children.Add(label);
        Canvas.SetLeft(label, gridX - 10);
        Canvas.SetTop(label, -20);
        gridX += dx;
    }
    // 添加水平网格
    while (gridY < backCanvas.Height)
    {
        line = new Line()
        {
            StrokeThickness = 0.1,
            Stroke = Brushes.Black,
            X1 = 0,
            Y1 = gridY,
            X2 = backCanvas.Width,
            Y2 = gridY
        };
        backCanvas.Children.Add(line);

        // 添加横坐标标签
        label = new Label();
        label.Content = gridY;
        backCanvas.Children.Add(label);
        Canvas.SetLeft(label, -40);
        Canvas.SetTop(label, gridY -10);
        gridY += dy;
    }
}

3、DrawRec(double width, double height)

为方块添加三个事件MouseLeftButtonDown、MouseMove、MouseLeftButtonUp
Draw4Lines : 同时画4条包裹它的直线,参数是该方块

private void DrawRec(double width, double height)
{
    // 方块
    Rectangle rec = new Rectangle()
    {
        Width = width,
        Height = height,
        Fill = new SolidColorBrush(Color.FromRgb(0, 24, 113))
    };
    foreCanvas.Children.Add(rec);
    Canvas.SetLeft(rec, foreCanvas.Width / 2 - rec.Width / 2);
    Canvas.SetTop(rec, foreCanvas.Height / 2 - rec.Height / 2);
    // 拖动方块事件
    rec.MouseLeftButtonDown += Rec_MouseLeftButtonDown;
    rec.MouseMove += Rec_MouseMove;
    rec.MouseLeftButtonUp += Rec_MouseLeftButtonUp;
    Draw4Lines(rec);
}

4、Draw4Lines(Rectangle rec)

4条直线由8个坐标点构成,pointList 就是存储该8个点,然后遍历画出,同时画4个由Label构成的坐标值。

private void Draw4Lines(Rectangle rec)
{
    List<Tuple<double, double>> pointList = new List<Tuple<double, double>>()
    {
        // 左竖线
        new Tuple<double, double>(Canvas.GetLeft(rec), 0),
        new Tuple<double, double>(Canvas.GetLeft(rec), foreCanvas.Height),
        // 右竖线
        new Tuple<double, double>(Canvas.GetLeft(rec) + rec.Width, 0),
        new Tuple<double, double>(Canvas.GetLeft(rec) + rec.Width, foreCanvas.Height),
        // 上横线
        new Tuple<double, double>(0, Canvas.GetTop(rec)),
        new Tuple<double, double>(foreCanvas.Width, Canvas.GetTop(rec)),
        // 下横线
        new Tuple<double, double>(0, Canvas.GetTop(rec) + rec.Height),
        new Tuple<double, double>(foreCanvas.Width, Canvas.GetTop(rec) + rec.Height)
    };
    for (int i = 0; i < pointList.Count; i += 2)
    {
        Line line = new Line()
        {
            StrokeThickness = 1,
            Stroke = new SolidColorBrush(Color.FromRgb(225, 88, 93)),
            X1 = pointList[i].Item1,
            Y1 = pointList[i].Item2,
            X2 = pointList[i + 1].Item1,
            Y2 = pointList[i + 1].Item2
        };
        foreCanvas.Children.Add(line);
        // 坐标值
        Label label = new Label()
        {
            Content = (i < 4) ? pointList[i].Item1 : pointList[i].Item2,
            Foreground = new SolidColorBrush(Color.FromRgb(255, 181, 73)),
            Background = new SolidColorBrush(Color.FromRgb(19, 51, 76))
        };
        foreCanvas.Children.Add(label);
        Canvas.SetLeft(label, pointList[i].Item1);
        Canvas.SetTop(label, pointList[i].Item2);
    }
}

5、鼠标事件

鼠标按下、释放,控制鼠标形状。

private void Rec_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    ((Rectangle)sender).Cursor = Cursors.Hand;
}

private void Rec_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    ((Rectangle)sender).ReleaseMouseCapture();
    ((Rectangle)sender).Cursor = Cursors.Arrow;
}

鼠标移动时,判断左键是按压状态;

获取鼠标的位置,计算出方块左上角顶点的位置,也就是 (marginLeft ,marginTop),同时判断是否超过边界;

如没有,则移动方块位置;

同时更新4条直线的位置。

private void Rec_MouseMove(object sender, MouseEventArgs e)
{
    if (e.LeftButton == MouseButtonState.Pressed)
    {
        // 鼠标位置
        Point point = e.GetPosition(foreCanvas);
        Rectangle rec = (Rectangle)sender;
        double marginLeft = point.X - rec.Width / 2;
        double marginTop = point.Y - rec.Height / 2;
        // 超出边界
        if (marginLeft < 0)
        {
            marginLeft = 0;
        }
        else if ((marginLeft + rec.Width) > foreCanvas.Width)
        {
            marginLeft = foreCanvas.Width - rec.Width;
        }
        if (marginTop < 0)
        {
            marginTop = 0;
        }
        else if ((marginTop + rec.Height) > foreCanvas.Height)
        {
            marginTop = foreCanvas.Height - rec.Height;
        }
        Canvas.SetLeft(rec, marginLeft);
        Canvas.SetTop(rec, marginTop);
        // 移除四条边框线,重新画
        foreCanvas.Children.RemoveRange(1, foreCanvas.Children.Count - 1);
        Draw4Lines(rec);
    }
}

小Bug

鼠标移动的太快,方块会不动
在这里插入图片描述

鼠标移动超过gridChart,例如超过右边界,此时不能上下移动,如下图,注意鼠标一直是按压状态
在这里插入图片描述

解决小bug

更新:2021-09-03
解决方法:使小方块强制获取到鼠标CaptureMouse

private void Rec_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    ((Rectangle)sender).CaptureMouse();
    ((Rectangle)sender).Cursor = Cursors.Hand;
}

因为,MouseMove事件,必须是鼠标处于控件之上才能发生,包括MouseDownMouseUp事件。
例如鼠标悬浮在控件之上,按下鼠标,此时会触发按下事件;此时鼠标移开到控件外,注意鼠标一直是按下状态,再松开,此时不会触发释放事件。因为鼠标不在控件之上~

解决后的效果,快的飞起~~~
在这里插入图片描述

原文链接 https://blog.csdn.net/pandawangyt/article/details/120064536

  • 6
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
WPF 中,可以使用鼠标事件和 Canvas.SetLeft / Canvas.SetTop 方法来实现拖动控件在 Canvas 上移动的效果。以下是一个示例: ```xaml <Canvas> <Button Content="Drag me!" Canvas.Left="50" Canvas.Top="50" MouseLeftButtonDown="Button_MouseLeftButtonDown" MouseMove="Button_MouseMove" MouseLeftButtonUp="Button_MouseLeftButtonUp"/> </Canvas> ``` 在代码中,我们为 Button 控件注册了三个鼠标事件:MouseLeftButtonDown、MouseMove 和 MouseLeftButtonUp。这三个事件将分别在鼠标按下、移动和松开时触发。 ```csharp private bool isDragging; private Point startPoint; private void Button_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { // 开始拖动 isDragging = true; startPoint = e.GetPosition(null); ((UIElement)sender).CaptureMouse(); } private void Button_MouseMove(object sender, MouseEventArgs e) { if (isDragging) { // 计算拖动距离 Point mousePos = e.GetPosition(null); Vector diff = startPoint - mousePos; // 移动控件 Button button = sender as Button; double left = Canvas.GetLeft(button); double top = Canvas.GetTop(button); Canvas.SetLeft(button, left - diff.X); Canvas.SetTop(button, top - diff.Y); // 更新起始点 startPoint = mousePos; } } private void Button_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { // 结束拖动 isDragging = false; ((UIElement)sender).ReleaseMouseCapture(); } ``` 在这个示例中,我们首先定义了两个变量:isDragging 和 startPoint。isDragging 变量表示当前是否正在拖动控件,startPoint 变量表示拖动开始时鼠标的位置。 在 MouseLeftButtonDown 事件中,我们将 isDragging 设为 true,并记录当前鼠标的位置。然后,我们调用 CaptureMouse 方法来捕获鼠标,以确保鼠标移动事件被正确处理。 在 MouseMove 事件中,我们首先判断当前是否正在拖动。如果是,就计算出当前鼠标位置和上一次鼠标位置的差值,然后使用 Canvas.SetLeft 和 Canvas.SetTop 方法更新控件的位置。最后,我们将当前鼠标位置保存到 startPoint 变量中,以便下一次计算差值。 在 MouseLeftButtonUp 事件中,我们将 isDragging 设为 false,并调用 ReleaseMouseCapture 方法来释放鼠标捕获。 通过这种方式,我们就可以实现在 Canvas 上拖动控件移动的效果。如果需要支持多个控件同时拖动,可以使用类似的方法为每个控件注册鼠标事件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值