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

news/2024/4/28 23:20:47/文章来源:https://blog.csdn.net/kfashfasf/article/details/137110980

在 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.luyixian.cn/news_show_1027434.aspx

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

相关文章

uniApp使用XR-Frame创建3D场景(7)加入点击交互

上篇文章讲述了如何将XR-Frame作为子组件集成到uniApp中使用 这篇我们讲解如何与场景中的模型交互&#xff08;点击识别&#xff09; 先看源码 <xr-scene render-system"alpha:true" bind:ready"handleReady"><xr-node><xr-mesh id"…

【已修复】iPhone13 Pro 长焦相机水印(黑斑)修复 洗水印

iPhone13 Pro 长焦相机水印&#xff08;黑斑&#xff09;修复 洗水印 问题描述 iPhone13 Pro 后摄3倍相机有黑色斑点&#xff08;水印&#xff09;&#xff0c;如图所示&#xff0c; 后摄相机布局如图所示&#xff0c; 修复过程 拆机过程有风险&#xff0c;没有把握最好不要…

MySQL学习笔记------函数

目录 函数 字符串函数 数值函数 函数 指一段可以直接被另一段程序调用的程序或代码 字符串函数 MySQL中内置了很多字符串函数&#xff0c;常用如下&#xff1a;select 函数 concat(s1,s2,s3,...,sn) 字符串拼接&#xff0c;将s1,s2,...,sn拼接成一个字符串 #字符串拼…

C/C++语言学习路线: 嵌入式开发、底层软件、操作系统方向(持续更新)

初级&#xff1a;用好手上的锤子 1 【感性】认识 C 系编程语言开发调试过程 1.1 视频教程点到为止 1.2 炫技视频看看就行 1.3 编程游戏不玩也罢 有些游戏的主题任务就是编程&#xff0c;游戏和实际应用环境有一定差异&#xff08;工具、操作流程&#xff09;&#xff0c;在…

火车头通过关键词采集文章的原理

随着互联网信息的爆炸式增长&#xff0c;网站管理员和内容创作者需要不断更新和发布新的文章&#xff0c;以吸引更多的用户和提升网站的排名。而火车头作为一款智能文章采集工具&#xff0c;在这一过程中发挥着重要作用。本文将探讨火车头如何通过关键词采集文章&#xff0c;以…

康耐视visionpro-CogCaliperTool工具详细说明

CogCaliperTool功能说明: 卡尺工具,用于测量距离 CogCaliperTool操作说明: ①.打开工具栏,双击或点击鼠标拖拽添加CogCaliperTool ②.添加输入图像,右键“链接到”或以连线拖拽的方式选择相应输入源 ③.拖动屏幕上的矩形框到需要测量的位置。卡尺的搜索框角度与边缘不平…

React系列之合成事件与事件处理机制

文章目录 React事件处理机制原生事件的事件机制事件代理&#xff08;事件委托&#xff09; 合成事件使用合成事件目的合成事件原生事件区别事件池 原生事件和React事件的执行顺序e.stopPropagation() React17事件机制的修改 React事件处理机制 react 事件机制基本理解&#xf…

有效的数独-java

题目描述: 请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 &#xff0c;验证已经填入的数字是否有效即可。 数字 1-9 在每一行只能出现一次。数字 1-9 在每一列只能出现一次。数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。&#xff08;请参考示例图&#…

什么是RISC-V?开源 ISA 如何重塑未来的处理器设计

RISC-V代表了处理器架构的范式转变&#xff0c;特点是其开源模型简化了设计理念并促进了全球community-driven的开发。RISC-V导致了处理器技术发展前进方式的重大转变&#xff0c;提供了一个不受传统复杂性阻碍的全新视角。 RISC-V起源于加州大学伯克利分校的学术起点&#xff…

RabbitMQ 实验消费原始队列消息, 拒绝(reject)投递死信交换机过程

如果你想通过 RabbitMQ 的死信队列功能实现消费者拒绝消息投递到死信交换机的行为&#xff0c;你可以按照以下步骤操作&#xff1a; 创建原始队列&#xff0c;并将其绑定到一个交换机上&#xff1a; export RABBITMQ_SERVER127.0.0.1 export RABBITMQ_PORT5672 export RAB…

uniApp使用XR-Frame创建3D场景(6)播放模型动画

上篇文章讲述了如何将XR-Frame作为子组件集成到uniApp中使用 这篇我们讲解播放模型动画 先看源码 <xr-scene render-system"alpha:true" bind:ready"handleReady"> <xr-node visible"{{sec6}}"><xr-light type"ambient&qu…

Mac 装 虚拟机 vmware、centos7等

vmware&#xff1a; https://www.vmware.com/products/fusion.html centos7 清华镜像&#xff1a; 暂时没有官方的 m1 arm架构镜像 centos7 链接: https://pan.baidu.com/s/1oZw1cLyl6Uo3lAD2_FqfEw?pwdzjt4 提取码: zjt4 复制这段内容后打开百度网盘手机App&#xff0c;操…

ida调试技巧-通过修改zf寄存器的值绕过简单反调试

参考本篇->OllyDbg笔记-对标志寄存器中ZF的理解&#xff08;逆向方面&#xff09;_零标志位zf怎么判断-CSDN博客 不想看也没关系&#xff0c;蒟蒻博主概述一下&#xff0c;总之&#xff0c;在机器执行汇编指令时&#xff0c;标志&#xff08;flag&#xff09;寄存器中的一个…

(4)(4.3) Kogger Sonar

文章目录 前言 1 推荐硬件 2 配置回声探测仪模块 3 连接ArduPilot硬件 4 参数说明 前言 KOGGER 声纳(KOGGER Sonar)是一款结构紧凑、成本低廉的水下回声测深仪模块&#xff0c;带有 UART 接口&#xff0c;电源电压为 5-14v。 1 推荐硬件 CP210x USB->UART 转换器和安装…

深度学习故障诊断实战 | 数据预处理之基于滑动窗的数据样本增强

前言 本期给大家分享介绍如何基于滑动窗方法进行数据样本增强 背景 深度学习模型训练需要大量的样本。在故障诊断领域&#xff0c;每个类别大都会达到300个样本。但是在实际公开数据集中&#xff0c;以CWRU数据集为例&#xff0c;每个类别只有24组数据&#xff0c;这明显是不…

HelpLook AI ChatBot:自定义Prompts综合指南

AI问答机器人&#xff08;AI Chatbot&#xff09;日益在各行业普及&#xff0c;但回答准确率的不足仍是其面临的痛点。用户在与AI问答机器人的互动中常发现&#xff0c;机器人难以完全理解和准确回答复杂问题。HelpLook可以通过自定义提示词&#xff08;Prompts&#xff09;和集…

【VMware Workstation】公司所有主机和虚拟机ip互通,以及虚拟机目录迁移

文章目录 1、场景2、环境3、实战3.1、所有主机和虚拟机ip互通Stage 1 : 【虚拟机】设置为桥接模式Stage 2 : 【虚拟机】设置ipStage 3 : 【路由器】ARP 静态绑定MACStage 3-1 ping 路由器 ipStage 3-2 【静态绑定】虚拟机查看mac地址Stage 3-3 【静态绑定】路由器ARP 静态绑定 …

eclipse自动跳到console 解决办法

eclipse启动服务后&#xff0c;想看一些properties信息或者别的&#xff0c;但老是自动跳转到console页面&#xff0c;下面是解决办法&#xff1a; Eclipse中按照如下顺序找到设置菜单的位置&#xff1a; Window — Preferences — Run/Debug — Console 找到以下两项&#xf…

阿里云CentOS7安装ZooKeeper单机模式

前提条件 阿里云CentOS7安装好jdk&#xff0c;可参 hadoop安装 的jdk安装部分 下载 [hadoopnode1 ~]$ cd softinstall [hadoopnode1 softinstall]$ wget https://archive.apache.org/dist/zookeeper/zookeeper-3.7.1/apache-zookeeper-3.7.1-bin.tar.gz 解压 [hadoopnode1 …

【论文阅读】FlipCAM:高分辨率遥感影像弱监督建筑物提取的特征级翻转增强方法

【论文阅读】FlipCAM&#xff1a;高分辨率遥感影像弱监督建筑物提取的特征级翻转增强方法 文章目录 【论文阅读】FlipCAM&#xff1a;高分辨率遥感影像弱监督建筑物提取的特征级翻转增强方法一、介绍二、方法2.1 准备2.2 一致性的架构2.3 SAM模块2.4 建筑提取模式 三、实验结果…