在 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>

WPF自定义Panel:让拖拽变得更简单的更多相关文章

  1. Moq让单元测试变得更简单

    [ASP.Net MVC3 ]使用Moq让单元测试变得更简单 前几天调查完了unity.现在给我的任务是让我调查Moq. 以下是自己找了资料,总结并实践的内容.如果有表述和理解错误的地方.恳请指正. ...

  2. 深入解析 Kubebuilder:让编写 CRD 变得更简单

    作者 | 刘洋(炎寻) 阿里云高级开发工程师 导读:自定义资源 CRD(Custom Resource Definition)可以扩展 Kubernetes API,掌握 CRD 是成为 Kubern ...

  3. 快开宝PDA开单器出入库扫码:让批发零售变得更简单

    快开宝PDA开单器出现前 批发商户是这样开单和管理的 ★员工痛苦:需要记客户.价格.库存等等,应对报错价.错漏单.盘错货等各种状况. ★老板麻烦:每天要守店.对单.核账,经常因错漏单.库存乱.积压货. ...

  4. spring 第一篇(1-1):让java开发变得更简单(下)

    切面(aspects)应用 DI能够让你的软件组件间保持松耦合,而面向切面编程(AOP)能够让你捕获到在整个应用中可重用的组件功能.在软件系统中,AOP通常被定义为提升关注点分离的一个技术.系统由很多 ...

  5. spring 第一篇(1-1):让java开发变得更简单(下)转

    spring 第一篇(1-1):让java开发变得更简单(下) 这个波主虽然只发了几篇,但是写的很好 上面一篇文章写的很好,其中提及到了Spring的jdbcTemplate,templet方式我之前 ...

  6. [翻译]Kafka Streams简介: 让流处理变得更简单

    Introducing Kafka Streams: Stream Processing Made Simple 这是Jay Kreps在三月写的一篇文章,用来介绍Kafka Streams.当时Ka ...

  7. Winform 让跨线程访问变得更简单

    Winform 让跨线程访问变得更简单 前言 由于多线程可能导致对控件访问的不一致,导致出现问题.C#中默认是要线程安全的,即在访问控件时需要首先判断是否跨线程,如果是跨线程的直接访问,在运行时会抛出 ...

  8. Kafka Streams简介: 让流处理变得更简单

    Introducing Kafka Streams: Stream Processing Made Simple 这是Jay Kreps在三月写的一篇文章,用来介绍Kafka Streams.当时Ka ...

  9. EpiiAdmin 开源的php交互性管理后台框架, 让复杂的交互变得更简单!Phper快速搭建交互性平台的开发框架,基于Thinkphp5.1+Adminlte3.0+Require.js。

    EpiiAdmin EpiiAdmin php开源交互性管理后台框架,基于Thinkphp5.1+Adminlte3.0+Require.js, 让复杂的交互变得更简单!Phper快速搭建交互性平台的 ...

  10. 让全链路压测变得更简单!Takin2.0重磅来袭!

    自Takin社区版1.0发布两个多月以来,有很多测试同学陆续在各自的工作中运用了起来,其中包括金融.电商.物流.出行服务等行业.这个过程中我们收到了很多同学的反馈建议,同时也了解到很多同学在落地全链路 ...

随机推荐

  1. 【Android】使用Messenger实现进程间通讯

    1 Messenger 简介 ​ Messenger 类实现了 Parcelable 接口,用于进程间传输并处理消息,调用流程如下: Client 通过 bindService() 请求绑定 Serv ...

  2. Java设计模式-命令模式Command

    介绍 命令模式(Command Pattern):在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收 者是谁,也不知道被请求的操作是哪个, 我们只需在程序运行时指定具体的请求接收者即 ...

  3. Golang Web 框架 Gin 基础学习教程集合目录

    Gin Web 框架基础学习系列目录 01-quickstart 02-parameter 03-route 04-middleware 05-log 06-logrus 07-bind 08-val ...

  4. Docker进阶之01-Docker Compose编排工具

    Docker Compose是什么 https://github.com/docker/compose 可以按项目为单位管理多个Docker容器,Python语言开发,底层调用Docker的API接口 ...

  5. 【LeetCode栈与队列#06】前K个高频元素(TopK问题),以及pair、priority_queue的使用

    前 K 个高频元素 力扣题目链接(opens new window) 给定一个非空的整数数组,返回其中出现频率前 k 高的元素. 示例 1: 输入: nums = [1,1,1,2,2,3], k = ...

  6. Golang条件编译介绍

    相信熟悉 Golang 的小伙伴不少都知道 条件编译 这个事,最近项目中也可能会用到这个东西.所以特意重新学习下,记录下学习的过程.这样用的时候记不住了,还可以直接过来看自己的笔记. 文章很多内容来源 ...

  7. Html飞机大战(十四): 分数编辑和生命值设定

    好家伙,这章让我感受到了面向对象的优势了   1.分数设置 每个种类的敌机分数都设置好了, 那么当我们击毁不同的敌机后,加上不同的分数就行了 但是我们还是要想一下,   我要在哪里放这个分数增加的方法 ...

  8. CentOS6.8下yum安装Nginx

    在/etc/yum.repos.d/目录下创建一个源配置文件nginx.repo: cd /etc/yum.repos.d/ vim nginx.repo 填写如下内容: [nginx] name=n ...

  9. 【Azure 应用服务】App Service中抓取 Web Job 的 DUMP 办法

    问题描述 使用Azure App Service,也可以部署一个Java程序作为Web Job运行.运行一个 .Jar 文件只需要以下4步: 1)把Java应用打包成一个 .jar 文件 2)创建一个 ...

  10. 【Azure 应用服务】在创建Web App Service的时候,选Linux系统后无法使用Mysql in App

    问题描述 如图上,是App Services在Windows环境中,系统自带了MySQL In App功能.而在,Linux环境中,没有发现Mysql in App功能,是不是无法在Linux中使用呢 ...