WPF自定义Panel:让拖拽变得更简单

在 WPF 应用程序中,拖放操作是实现用户交互的重要组成部分。通过拖放操作,用户可以轻松地将数据从一个位置移动到另一个位置,或者将控件从一个容器移动到另一个容器。然而,WPF 中默认的拖放操作可能并不是那么好用。为了解决这个问题,我们可以自定义一个 Panel 来实现更简单的拖拽操作。

自定义 Panel 的优点有很多。首先,我们可以根据自己的需求来设计 Panel 的外观和行为。其次,我们可以使用代码来控制拖放操作的细节,比如拖放的开始和结束位置、拖放过程中控件的显示方式等等。最后,我们可以将自定义 Panel 作为一个控件,方便地应用到不同的应用程序中。 在本教程中,我们将一步一步地创建一个自定义 Panel 来实现更简单的拖拽操作。我们将学习如何定义 Panel 的布局、如何处理拖放事件,以及如何将自定义 Panel 应用到不同的应用程序中。按照本教程的步骤操作,您将能够创建一个功能强大且易于使用的自定义 Panel,从而使您的 WPF 应用程序更加友好和易用。

1.定义一个继承自Panel的类。

public class DragStackPanel : Panel
{
    /// <summary>
    /// 获取或设置方向
    /// </summary>
    public Orientation Orientation
    {
        get { return (Orientation)GetValue(OrientationProperty); }
        set { SetValue(OrientationProperty, value); }
    }

    public static readonly DependencyProperty OrientationProperty =
        DependencyProperty.Register("Orientation", typeof(Orientation), typeof(DragStackPanel), new PropertyMetadata(Orientation.Vertical));
}

2.重写Panel类的MeasureOverride方法测量控件Size。

public class DragStackPanel : Panel
{
    protected override Size MeasureOverride(Size availableSize)
    {
        var panelDesiredSize = new Size();
        foreach (UIElement child in InternalChildren)
        {
            child.Measure(availableSize);
            if (this.Orientation == Orientation.Horizontal)
            {
                panelDesiredSize.Width += child.DesiredSize.Width;
                panelDesiredSize.Height = double.IsInfinity(availableSize.Height) ? child.DesiredSize.Height : availableSize.Height;
            }
            else
            {
                panelDesiredSize.Width = double.IsInfinity(availableSize.Width) ? child.DesiredSize.Width : availableSize.Width;
                panelDesiredSize.Height += child.DesiredSize.Height;
            }
        }
        return panelDesiredSize;
    }
}

3.重写Panel类的ArrangeOverride方法排列控件位置。

public class DragStackPanel : Panel
{
    protected override Size ArrangeOverride(Size finalSize)
    {
        double x = 0, y = 0;
        foreach (FrameworkElement child in InternalChildren)
        {
            // 坐标
            var position = new Point(x, y);
            // 宽度
            var width = child.DesiredSize.Width;
            // 高度
            var height = child.DesiredSize.Height;
            // 通过排列方向计算宽度和高度
            if (this.Orientation == Orientation.Vertical)
            {
                width = finalSize.Width;
            }
            else
            {
                height = finalSize.Height;
            }

            // 尺寸
            var size = new Size(width, height);
            // 排列位置及尺寸
            child.Arrange(new Rect(position, size));

            // 计算位置
            if (this.Orientation == Orientation.Horizontal)
            {
                x += child.DesiredSize.Width;
            }
            else
            {
                y += child.DesiredSize.Height;
            }
        }

        return finalSize;
    }
}

查看运行效果

<UniformGrid Rows="2">
    <local:DragStackPanel Orientation="Horizontal">
        <Button>test1</Button>
        <Button>test2</Button>
    </local:DragStackPanel>
    <local:DragStackPanel Orientation="Vertical">
        <Button>test3</Button>
        <Button>test4</Button>
    </local:DragStackPanel>
</UniformGrid>

4.重写PreviewMouseLeftButtonDown方法。

该方法在按下鼠标左键时触发,我们需要在该方法中获取第一次按下鼠标的坐标,并且通过命中测试找到我们要拖拽的控件,最后还要在装饰层中添加一个元素,该元素的背景用原控件的外观来填充(VisualBrush),这样就可以覆盖原来的控件,以便在拖拽控件时能跨越控件的边界。以下为参考代码:

public class DragStackPanel : Panel
{
    private FrameworkElement draggingElement;
    private Point mouseRelativePosition;
    private int draggingElementzIndex;
    protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        // 获取鼠标相对于Panel的坐标
        var mousePosition = e.GetPosition(this);
        // 通过命中测试获取当前鼠标位置下的元素
        var hitTestResult = this.InputHitTest(mousePosition) as FrameworkElement;
        // 通过命中测试结果找到当前拖拽的控件子项
        draggingElement = FindChild(hitTestResult);
        if (draggingElement != null && this.InternalChildren.Contains(draggingElement))
        {
            // 记录鼠标相对位置,以供后续使用
            mouseRelativePosition = e.GetPosition(draggingElement);

            // 暂存ZIndex
            draggingElementzIndex = Panel.GetZIndex(draggingElement);
            // 将ZIndex置顶
            Panel.SetZIndex(draggingElement, this.InternalChildren.Count);
            // 添加遮罩,防止拖拽时覆盖
            AddOverlay(draggingElement);

            e.Handled = true;
        }

        base.OnPreviewMouseLeftButtonDown(e);
    }
}

5.重写PreviewMouseMove方法。

该方法在鼠标移动时触发,我们需要在鼠标被按下移动时,根据当前的坐标与第一次按下的坐标实时计算出被拖拽元素的偏移量,这样该元素就能跟随鼠标移动,实现拖拽效果。以下为参考代码:

public class DragStackPanel : Panel
{
    private FrameworkElement draggingElement;
    private Point mouseRelativePosition;
    private int draggingElementzIndex;
    protected override void OnPreviewMouseMove(MouseEventArgs e)
    {
        var mousePosition = e.GetPosition(this);
        if (e.LeftButton == MouseButtonState.Pressed && draggingElement != null)
        {
            // 当前拖拽控件置为不可鼠标命中,以供命中下一层的换位控件
            draggingElement.IsHitTestVisible = false;
            // 判断当前拖拽的控件是否为顶层控件
            if (Panel.GetZIndex(draggingElement) == this.InternalChildren.Count)
            {
                // 计算出当前拖拽控件相对于this的位置(控件左上角)
                var targetPosition = new Point(mousePosition.X - mouseRelativePosition.X - draggingElement.Margin.Left, mousePosition.Y - mouseRelativePosition.Y - draggingElement.Margin.Top);
                // 获取当前拖拽控件在this中的原始位置
                var draggingElementOriginalPosition = GetDraggingElementOriginalPosition(draggingElement);
                // 计算拖拽控件移动时的偏移量
                var offset = new Point(targetPosition.X - draggingElementOriginalPosition.X, targetPosition.Y - draggingElementOriginalPosition.Y);
                // 应用位移
                draggingElement.RenderTransform = new TranslateTransform(offset.X, offset.Y);
            }
            
             e.Handled = true;
        }
        base.OnPreviewMouseMove(e);
    }
}

6.重写PreviewMouseLeftButtonUp方法。

该方法在鼠标左健抬起时触发,我们需要在该方法中将一些参数重置。

public class DragStackPanel : Panel
{
    private FrameworkElement draggingElement;
    private Point mouseRelativePosition;
    private int draggingElementzIndex;
    protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
    {
        mouseRelativePosition = default;
        RemoveOverlay(draggingElement);
        Panel.SetZIndex(draggingElement, draggingElementzIndex);
        draggingElement.IsHitTestVisible = true;
        draggingElement.RenderTransform = null;
        draggingElement = null;
        e.Handled = true;
        base.OnPreviewMouseLeftButtonUp(e);
    }
}

以下为运行效果:

7.处理控件的拖拽换位。

拖拽换位的思路就是将当前正在拖拽的元素放置到新的Index中,并把该Index后面的所有元素整体后移一位。该功能在PreviewMouseMove方法中实现。

public class DragStackPanel : Panel
{
    private FrameworkElement draggingElement;
    private FrameworkElement hitElement;
    private Point mouseRelativePosition;
    private int draggingElementzIndex;
    protected override void OnPreviewMouseMove(MouseButtonEventArgs e)
    {
        ...
        // 命中当前拖拽控件的下一层控件
        var hitTestResult = this.InputHitTest(mousePosition) as FrameworkElement;
        // 查找被命中的下一层换位控件
        hitElement = FindChild(hitTestResult);

        // 判断是否有效
        if (hitElement != null && this.InternalChildren.Contains(hitElement))
        {
            // 应用换位
            MoveChild(draggingElement, hitElement);
        }
    }

    private void MoveChild(FrameworkElement element1, FrameworkElement element2)
    {
        var index1 = this.InternalChildren.IndexOf(element1);
        var index2 = this.InternalChildren.IndexOf(element2);
        if (index1 >= 0 && index2 >= 0)
        {
            this.InternalChildren.RemoveAt(index1);
            this.InternalChildren.Insert(index2, element1);
        }
    }
}

在ArrangeOverride方法中处理重新排列时当前拖拽元素的坐标。

public class DragStackPanel : Panel
{
    private FrameworkElement draggingElement;
    private FrameworkElement hitElement;
    private Point mouseRelativePosition;
    private int draggingElementzIndex;
    protected override Size ArrangeOverride(Size finalSize)
    {
        double x = 0, y = 0;
        foreach (FrameworkElement child in InternalChildren)
        {
            ...

            // 获取当前正在拖拽元素的位置坐标
            var dragElementPosition = GetDraggingElementMovingPosition(child);
            if (dragElementPosition != default)
            {
                // 处理拖拽元素坐标
                var offset = new Point(dragElementPosition.X - position.X, dragElementPosition.Y - position.Y);
                child.RenderTransform = new TranslateTransform(offset.X, offset.Y);
                SetDraggingElementMovingPosition(child, dragElementPosition);
            }

            ...
        }

        return finalSize;
    }
}

运行效果

8.处理跨Panel拖拽。

到目前为止已经实现了本Panel内的控件随意拖拽换位,处理从A控件拖到B控件也类似,这里需要用到一个静态变量来保存正在拖拽的控件,当B控件检测到鼠标进入时,只需要在A控件移除正在拖拽的控件,在B控件添加正在拖拽的控件就可以实现了。以下为核心代码:

public class DragStackPanel : Panel
{
    // 通过拖拽传递到下一个Panel的控件
    private static FrameworkElement draggingTransferElement;
    private void Control_MouseEnter(object sender, MouseEventArgs e)
    {
        panel.Children.Remove(draggingTransferElement);
        panel.DraggingElement = null;

        Panel.SetZIndex(draggingTransferElement, this.InternalChildren.Count + 1);
        this.Children.Add(draggingTransferElement);
        this.AddOverlay(draggingTransferElement);
    }
}

以下为运行效果:

9.在ListBox、ListView、DataGrid等ItemsControl中使用拖拽功能。

所有继承自ItemsControl的控件,都有一个ItemsPanel属性,该属性可以指定一个Panel类型的控件来对ItemsControl进行排列。理论上只要将ItemsControl.ItemsPanel设置为我们自己开发的Panel控件就可以实现排列及拖拽功能,但是这里直接使用的话并不会有效果。原因就是我们并没有对数据绑定的情况下做处理。它的处理逻辑也与上面的类似,首先找到ItemsControl控件,通过对ItemsSource进行操作就可以实现排列功能,由于代码大同小异这里就不再赘述。以下为ListBox控件拖拽的案例效果。

<ListBox ItemsSource="{Binding Items}">
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <DragStackPanel AllowCrossBorderDrag="True" CanDragAndSort="True" IsItemsHost="True"/>
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Property1}" />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

10.添加动画效果。

至此基本功能已经开发完成了,下面我们为它添加上动画效果,让它更具有观赏性。动画的核心思想就是记录每个元素旧位置的坐标,当元素移动到新位置时启动一个动画,从旧坐标过渡到新坐标,由于代码太过基础,这里就不展示了,直接上效果。

<DragStackPanel AllowCrossBorderDrag="True" CanDragAndSort="True" IsItemsHost="True">
    <DragStackPanel.ChildMoveBehavior>
        <ChildMoveBehavior Duration="0:0:0.5">
            <ChildMoveBehavior.EaseX>
                <QuinticEase EasingMode="EaseOut" />
            </ChildMoveBehavior.EaseX>
            <ChildMoveBehavior.EaseY>
                <QuinticEase EasingMode="EaseOut" />
            </ChildMoveBehavior.EaseY>
        </ChildMoveBehavior>
    </DragStackPanel.ChildMoveBehavior>
</DragStackPanel>

文章转载自:趋时软件

原文链接:https://www.cnblogs.com/qushi2020/p/18098514

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/495955.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

http接口测试—自动化测试框架设计(超详细)

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 关注公众号【互联网杂货铺】&#xff0c;回复 1 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 一、测试需求描述 对服务后台一系列的http接口功能测试。 …

【Git篇】复习git

文章目录 &#x1f354;什么是git⭐git和svn的区别 &#x1f354;搭建本地仓库&#x1f354;克隆远程仓库&#x1f6f8;git常用命令 &#x1f354;什么是git Git是一种分布式版本控制系统&#xff0c;它可以追踪文件的变化、协调多人在同一个项目上的工作、恢复文件的旧版本等…

微信开发者工具创建一个小程序

创建项目 对于上面这个AppID可以自行选择是注册还是测试号&#xff0c;我是使用的测试号&#xff0c;之后再下面选择模板&#xff0c;我这里选择了JS-基础模板。 进入项目后在模拟器中可看到如下页面&#xff1a; 添加提交按钮进行页面跳转 添加需要跳转的文件夹&#xff0c;…

更新时间后OpenStack neutron 401 Unauthorized解决办法

发现时间跟现实时间有偏差&#xff0c;用 ntpdate cn.pool.ntp.org 更新时间后再用neutron 发现报错 401-{uerror: {umessage: uThe request you have made requires authentication., ucode: 401, utitle: uUnauthorized}} 而且用的是账号密码的认证&#xff0c;还是无法正…

跑通飞浆平台的MTMCT 跨镜跟踪示例

想跑通飞浆平台的MTMCT跨镜跟踪示例&#xff0c;真的是难上加难啊&#xff01; 改了几处代码&#xff0c;可以顺利跑通了&#xff0c;特此记录&#xff1a; 第一处&#xff1a;不要拉主线的代码&#xff0c;改成 !git clone https://gitee.com/paddlepaddle/PaddleDetection…

Wagtail-基于Python Django的内容管理系统CMS实现公网访问

目录 ⛳️推荐 前言 1. 安装并运行Wagtail 1.1 创建并激活虚拟环境 2. 安装cpolar内网穿透工具 3. 实现Wagtail公网访问 4. 固定Wagtail公网地址 ⛳️推荐 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给…

【vue3学习笔记(一)】vue3简介;使用vue-cli创建工程;使用vite创建工程;分析工程结构;安装开发者工具

尚硅谷Vue2.0Vue3.0全套教程丨vuejs从入门到精通 对应课程136-140节 课程 P136节 《vue3简介》笔记 课程 P137节 《使用vue-cli创建工程》笔记 官方文档&#xff1a; https://cli.vuejs.org/zh/guide/creating-a-project.html#vue-create官方文档地址 查看vue-cli版本&#x…

人大金仓数据库介绍与使用指南

人大金仓数据库是一款强大的关系型数据库管理系统&#xff0c;具有简单易用、高性能和稳定可靠的特点。本文将介绍人大金仓数据库的安装方法、常用的SQL语法以及相关工具的使用。 一、安装方法&#xff1a; 1、下载人大金仓数据库安装程序&#xff1b; 2、运行安装程序&#…

Visio中存在问题的解决方法

公式缩放 mathtype公式在visio缩放之后&#xff0c;出现了变形。 解决方法&#xff1a;每次输入公式都通过 插入->对象->mathType Equation 新建一个公式。可以避免 注&#xff1a;网上有的说在word中使用mathtype编写公式&#xff0c;之后复制到visio中。 插入波形 选择…

如何制定具有挑战性的绩效目标,同时又能激励员工积极投入工作?

在现代企业管理中&#xff0c;绩效目标的设定不仅是评价员工工作成果的依据&#xff0c;更是激励员工积极投入工作的重要手段。然而&#xff0c;如何制定出既具有挑战性又能激励员工的目标&#xff0c;往往成为管理者需要深思熟虑的问题。本文将探讨如何平衡这两点&#xff0c;…

matplotlib 绘图

matplotlib 绘图 方便设置legend图例的位置 ax1.legend(loc‘upper center’, bbox_to_anchor(0.3, -0.1)) ax2.legend(loc‘upper center’, bbox_to_anchor(0.6, -0.1)) import numpy as np import matplotlib.pyplot as plt from scipy.stats import norm from scipy.inter…

金融案例:构建高效统一的需求登记与管理方案

在金融行业数字化转型背景下&#xff0c;银行等金融机构面临着业务模式创新与数据应用的深度融合。业务上所需要的不再是单纯的数据&#xff0c;而是数据背后映射的业务趋势洞察&#xff0c;只有和业务相结合转化为业务度量指标&#xff0c;经过数据分析处理呈现为报表进行展示…

一键永久存档,帕鲁冒险永不丢失

天生不爱笑的瞅什魔、最强打手炎魔羊、跑图之王云海鹿、万能配种棉悠悠...... 真的永远不想和这些可爱的帕鲁说再见&#xff5e; 那么&#xff0c;如果服务器快过期了&#xff0c;如何永久保存游戏&#xff0c;保留我们和帕鲁的美好回忆呢&#xff1f;本教程说明如何使用轻量对…

如何利用OpenCV4.9 更改图像的对比度和亮度

返回&#xff1a;OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇&#xff1a;使用 OpenCV 添加&#xff08;混合&#xff09;两个图像 下一篇:如何利用OpenCV4.9离散傅里叶变换 ​目标 在本教程中&#xff0c;您将学习如何&#xff1a; 访问像素值用零…

鸿蒙OS开发实例:【工具类封装-首选项本地存储】

import dataPreferences from ohos.data.preferences; import bundleManager from ohos.bundle.bundleManager; 本地首选项数据的保存&#xff0c;利用key value 【使用要求】 DevEco Studio 3.1.1 Release api 9 【使用示例】 1、app启动时&#xff0c;从本地读取数据&…

Spring Integration 是什么?

Spring Integration 是什么&#xff1f; Spring Integration 在 Spring 家族不太有名气&#xff0c;如果不是有需求&#xff0c;一般也不会仔细去看。那么 Spring Integration 是什么呢&#xff1f;用官方的一句话来解释就是&#xff1a;它是一种轻量级消息传递模块&#xff0…

干货分享:品牌如何通过社媒激发年轻人消费力?

随着年轻人的消费愈发理性&#xff0c;年轻人在消费时更偏向于熟人种草场景下的信任决策&#xff0c;社交媒体成为品牌吸引用户的必争之地。今天媒介盒子就来和大家聊聊&#xff1a;品牌如何通过社媒激发年轻人消费力。 一、 激发用户共鸣&#xff0c;与用户产生情感连接。 虽…

通达信指标公式--通达信波段极品抄底指标公式,波段顶部和底部的判断

今日分享的通达信波段极品抄底指标公式&#xff0c;是一个波段顶底的提示指标。 具体信号说明&#xff1a; 当指标信号柱出现洋红柱子时&#xff0c;是波段底的信号&#xff0c;此时是参考持股的信号。 抄底信号出现在黄色直线附近较好&#xff0c;出现在背离走势更好。懂波浪…

【正点原子FreeRTOS学习笔记】————(12)信号量

这里写目录标题 一、信号量的简介&#xff08;了解&#xff09;二、二值信号量&#xff08;熟悉&#xff09;三、二值信号量实验&#xff08;掌握&#xff09;四、计数型信号量&#xff08;熟悉&#xff09;五、计数型信号量实验&#xff08;掌握&#xff09;六、优先级翻转简介…

[优选算法专栏]专题十五:FloodFill算法(一)

本专栏内容为&#xff1a;算法学习专栏&#xff0c;分为优选算法专栏&#xff0c;贪心算法专栏&#xff0c;动态规划专栏以及递归&#xff0c;搜索与回溯算法专栏四部分。 通过本专栏的深入学习&#xff0c;你可以了解并掌握算法。 &#x1f493;博主csdn个人主页&#xff1a;小…