在 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. NC23501 小A的回文串

    题目链接 题目 题目描述 小A非常喜欢回文串,当然我们都知道回文串这种情况是非常特殊的.所以小A只想知道给定的一个字符串的最大回文子串是多少,但是小A对这个结果并不是非常满意.现在小A可以对这个字符串 ...

  2. JavaScript 的灵异事件之一

    场景 在做项目的时候需要用到Ajax 做多次的异步处理数据, 三次Ajax:A --ok--> B --ok--> C 在入参数据相同的情况下,做了两论这个操作,但发现没有发送 A 的 A ...

  3. Event对象

    Event对象 Event对象表示在DOM中出现的事件,在DOM中有许多不同类型的事件,其主要使用基于Event对象作为主接口的二次接口,Event对象本身包含适用于所有事件的属性和方法. 描述 事件 ...

  4. SuspendThread and ResumeThread

    记录下,用于复现 #include <windows.h> #include <iostream> LRESULT CALLBACK WndProc(HWND hWnd, UI ...

  5. C++ 线程的学习---线程同步

    因为是学习篇,写下是为了个人的学习与理解.故参考其他文章为多. 为什么需要线程同步? 在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的操作.更多的情况是一些线程进行某些处理操作,而 ...

  6. django学习第一天---MVC和MTV框架,request对象的属性,url路由系统

    jinja2模板渲染简单使用 下载安装 pip install jinja2 使用示例 html文件中写法 <!DOCTYPE html> <html lang="zh-C ...

  7. vim创建sh文件自动生成头信息

    >>> vim /etc/vimrc 或 vim ~/.vimrc " 这几个加不加都行 set tabstop=4 set softtabstop=4 set shift ...

  8. chrome浏览器配置自定义搜索引擎

    chrome谷歌浏览器配置自定义搜索引擎 放弃百度搜索已经酝酿许久,现在搜索结果简直不忍直视.如果你想放弃使用百度搜索,并转向其他搜索引擎,头条搜索可能是一个不错的选择. 使用以下方式可以丝滑的使用其 ...

  9. 【Azure 应用服务】查看App Service for Linux上部署PHP 7.4 和 8.0时,所使用的WEB服务器是什么?

    问题描述 如何查看PHP应用部署到App Service后,Azure上面使用的应用服务器是什么呢?因为App Service支持两种操作系统,Windows 和 Linux.在Windows中,使用 ...

  10. C++11新特性的一些用法举例①

    //字符串字面量/*常用:1.原始字符串字面量 --- 括号内保持原样输出 --- 没有转义字符,如\n不再是换行,而是直接输出字面量\nR"(str)"; 实例: R" ...