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);// 暂存ZIndexdraggingElementzIndex = 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快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

智慧公厕的技术融合策略

智慧公厕是迎合现代城市发展需要的一项重要基础设施&#xff0c;其设计的技术融合策略在实现公共厕所泛在感知、互通互联、协同构筑智慧城市等方面起到了关键作用。本文将以智慧公厕源头实力厂家广州中期科技有限公司&#xff0c;大量精品案例现场实景实图实例&#xff0c;从物…

Mathematica使用笔记

文章目录 1. 数组定义及访问2. 表达式替换3. 清除变量4. 匿名函数&#xff08;纯函数&#xff09;5. 化简6. 求导7. 积分8. 求解方程及方程组8.1 求解方程形式8.2 求解方程组形式8.3 设置根式解显示方式 9. 表达式转C10. 提取多项式系数 1. 数组定义及访问 A {1,2,3} B {{1,…

数字兆欧表的量程选择技巧,你需要知道的秘密!

使用数字兆欧表的方法如下&#xff1a; 步骤一&#xff1a;准备工作 在开始操作之前&#xff0c;务必确保待测电气设备已经断电并进行了充分放电&#xff0c;以保证操作的安全性。此外&#xff0c;还需检查数字兆欧表的电量是否足够&#xff0c;外观是否完好&#xff0c;并确保…

元宇宙虚拟展厅——虚拟场景同现实世界的结合宣传

元宇宙作为具有前瞻性和先导性的未来产业&#xff0c;在政策布局、产业发展以及应用融合等方面也都实现了诸多探索。元宇宙虚拟展厅是虚拟场景同现实世界的结合&#xff0c;元宇宙与展厅的应用融合打破了传统展厅、传统宣传对时间和空间的限制&#xff0c;以往我们印刷过数不胜…

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

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

MySQL 8 索引原理详细分析

千山万水总是情, 问问索引行不行? 轻舟已过万重山, 有种尽管来发难。 索引是在数据库优化时的重要手段之一,今天 V 哥从索引的角度展开讲一讲索引的各个要点,希望可以通过这篇文章,帮助大家彻底搞透索引的关键点。 1.索引的定义与作用2.索引的类型3.索引原理4.二分查…

C语言模拟试题一

模拟试题一 一、选择题(本大题30 分,每小题2 分) 1下列说法正确的是。 E) main函数必须放在C程序的开头 F) main函数必须放在C程序的最后 G) main函数可以放在C程序的中间部分,但在执行C程序时是从程序开头执 行的 H) main函数可以放在C 程序的中间部分,但在执行C 程序时…

Redis 不再“开源”:中国面临的挑战与策略应对

Redis 不再“开源”&#xff0c;使用双许可证 3 月 20 号&#xff0c;Redis 的 CEO Rowan Trollope 在官网上宣布了《Redis 采用双源许可证》的消息。他表示&#xff0c;今后 Redis 的所有新版本都将使用开源代码可用的许可证&#xff0c;不再使用 BSD 协议&#xff0c;而是采用…

leetcode 1035.不相交的线

思路&#xff1a;最长公共子序列的动态规划问题。 其实简单来说就是将最长公共子序列那道题的字符串改成了数字而已&#xff0c;其实都是一样的&#xff0c;那么我就在这里不多讲了&#xff0c;如果要看思路可以看博主这篇leetcode 1143.最长公共子序列-CSDN博客 注意&#x…

玩一下Proxmox,硬盘扩容

问题 今天用proxmox系统新建了一个debian系统,当时只是见识下,发现还很好用,于是就又加了一块存储盘,如何扩容,过程记录一下。 扩容过程 1.首先要给proxmox添加硬盘,是一个旧的,所以分区怎么也删不干净,只好用下面的命令清零 dd if=/dev/zero of=/dev/sdb bs=512 co…

C++进阶学习(5)继承中的重名成员与静态成员

文章目录 一、重名成员1.重名数据成员2.重名成员函数 二、派生类中访问静态成员 一、重名成员 派生类定义了与基类同名的成员&#xff0c;在派生类中访问同名成员时屏蔽了基类的同名成员 在派生类中使用基类的同名成员&#xff0c;显式地使用类名限定符&#xff1a; 类名 :: …

dapr学习

dapr学习 5.1、概念知识 官方文档&#xff1a; Dapr 配置选项概述 | Dapr 文档库 run CLI 命令参考 | Dapr 文档库 该部分持续更新中… dapr云原生框架&#xff0c;服务于开发人员的&#xff0c;是一个cs服务框架 5.2、dapr配置redis中间件组件 1、首先下载redis进行并…

国产算力芯片排行榜前十名

算力芯片是指用于进行大规模计算的集成电路芯片&#xff0c;它的性能和效率将直接影响到人工智能、大数据分析、密码学和区块链等领域的发展。近年来&#xff0c;随着国内算力芯片制造企业的迅速崛起&#xff0c;国产算力芯片在技术研发和市场表现上都取得了显著的进步。下面就…

CBO VS ABO,哪种策略才更能优化FB广告?

海外创业时&#xff0c;FB广告无疑是吸引目标受众、推动业务增长的重要渠道之一&#xff01;然而令大家头疼的却是在CBO与ABO的选择上&#xff0c;今天就带大家一起解读这两种常见的广告策略。了解两者之间的区别、优缺点及适用场景。 CBO 和 ABO 分别是什么&#xff1f; CBO&a…

从数据治理到数据资产管理,释放新质生产力

在当前的数字化浪潮中&#xff0c;数据作为第五大生产要素&#xff0c;正日益成为企业激活新质生产力的关键引擎。数据治理和数据资产管理作为数据管理的重要环节&#xff0c;对企业提升竞争力、实现数字化转型具有重大意义。本文将对数据治理和数据资产管理进行深入探讨&#…

vue3封装Element动态表单组件

1. 封装组件DymanicForm.vue 使用component实现动态组件组件不能直接使用字符串传入&#xff0c;所以根据传入的组件名称找到对应的组件校验规则&#xff0c;可使用rule传入自定义规则&#xff0c;也可以使用封装好的基本规则 示例中使用了checkRequired暴露重置方法和校验方法…

python_1

要求&#xff1a; 代码&#xff1a; # 先将分钟数转化成年数&#xff0c;再将余数做为天数 minute float(input("请输入分钟数&#xff1a;")) year_1 (minute / 60 / 24) // 365 day_1 (minute / 60 / 24) % 365 now f"{minute}分钟{year_1}年{day_1}天&q…

Cocos2dx-lua ScrollView[三]高级篇

一.概述 本文缩写说明:sv = ScrollView, cell代表ScrollView的一个子节点 本文介绍sv的一种封装类库,来实现快速创建sv,有如下几个优点: 1.item的位置通过参数控制,提高开发效率 2.免去了调用sv的API,提高开发效率 3.分帧创建,提高性能 4.可通过参数控制,复用ite…

250.voc,vok 声音,叫喊

250.voc,vok 声音,叫喊 vocal  【vəukəl】 adj. 【voc 声,-al …的】 有声的;用语言表达的,口述的;歌唱的 vocalism  【vəukəlizəm】 n. 【见上,-ism 表行为】 发声;歌唱 vocalist  【vəukəlist】 n. 【见上,-ist 者】 歌唱者;声乐家 vocalize  【vəukə…

《Python编程快速上手——让繁琐工作自动化》实践项目——物品清单

题目&#xff1a; 整个题目篇幅比较大。简化一下就是编写addToInventory函数并且返回一个字典。 思路&#xff1a;函数displayInventory用于物品清单的最后输出&#xff0c;而addToInventory是用于处理战利品的&#xff0c;在原来的装备上添加&#xff0c;已有的装备直接数量加…