当前位置: 首页 > news >正文

开发集合控件的拖拽流程优化——以TreeView为例

简介

文章不会介绍简单的拖拽开发流程,而是记录如何在已有拖拽控件上进一步优化,提高控件的性能和使用体验。具体的优化内容主要涉及到一下几个方面。代码中使用到的工具类都会在文章末尾给出。

  1. 虚拟化提高控件加载性能。
  2. 拖拽操作的防误触。
  3. 拖拽时鼠标的样式修改
  4. 在拖拽时的高光显示以及靠近上端或低端时滚动条自动滚动。
  5. 拖拽结点的位置移动。

虚拟化

很多时候,当我们一次在集合控件中添加大量元素后,会导致程序卡顿甚至停止响应,并且UI的渲染速度也十分缓慢。这时候就需要用到虚拟化技术。

VirtualizingPanel

VirtualizingPanel是WPF中一个特殊的面板抽象基类,它的核心功能是仅渲染当前可见区域内的元素,而非所有数据项。该类中提供了与虚拟化相关的参数设置。

参数 描述
VirtualizingPanel.CacheLength 控件需要缓存的项目数。这意味着在视口之外的区域中,面板会保留一定数量的项目以提高滚动平滑度。
VirtualizingPanel.CacheLengthUnit CacheLengthUnit 属性定义 CacheLength 的单位。其中 Item 表示缓存的长度以项目的数量为单位,Pixel 表示缓存的长度以像素为单位。
VirtualizingPanel.IsContainerVirtualizable 当元素滚动出可见区域时,面板会尝试回收这些元素的容器(如 ListBoxItem、TreeViewItem)以节省资源。IsContainerVirtualizable 属性用于指定:某个具体的容器元素是否允许被虚拟化面板回收。
VirtualizingPanel.IsVirtualizing 面板是否启用虚拟化。这是虚拟化的核心设置,设置为 True 表示面板会仅对视口内的项目进行渲染和处理,而不是一次性加载所有项目。
VirtualizingPanel.IsVirtualizingWhenGrouping 面板在分组时是否继续进行虚拟化。当设置为 True 时,面板在分组数据时仍然会应用虚拟化策略,以保持性能优化。
VirtualizingPanel.ScrollUnit 定义滚动的单位。可以选择 Item 或 Pixel,其中 Item 表示每次滚动一个项目,Pixel 表示每次滚动一定像素。值:Item 表示每次滚动一个项目的单位,而不是固定像素数,这对于项目高度一致的情况尤其有效。
VirtualizingPanel.VirtualizationMode 指定虚拟化模式。Recycling 模式表示控件会重用已经不再可见的项目的容器,而不是销毁它们。

VirtualizingStackPanel

VirtualizingStackPanel 是 WPF 中最常用的虚拟化面板控件,专为高效处理大量数据项的列表场景 ** 设计,通过 “仅渲染可见区域元素” 的机制显著提升性能。它是 VirtualizingPanel 的子类。WPF中的部分集合控件(ListBox、ListView、DataGrid、TreeView都已经默认使用该控件)。 但对于,TreeView仅针对展开的节点层级生效。 未展开的的一级结点则不会采用虚拟化。

如何使用虚拟化

需要明确一点,如果在控件中没有使用继承VirtualizingPanel的虚拟化控件(如VirtualizingStackPanel),而是仅仅通过VirtualizingPanel设置了相关的虚拟化属性,那么控件是不会具备虚拟化效果的。
通过上面的介绍可知,已经有部分控件完全实现了虚拟化的支持。但对于ItemsControl,由于其使用场景以及作为集合控件的基类,WPF中并未直接支持其虚拟化,需要手动设置其ItemsPanelTemplate,来实现虚拟化。

点击查看代码
 <ScrollViewer Height="300"> <!-- 提供滚动容器 --><ItemsControl ItemsSource="{Binding LargeDataSource}"><!-- 替换为虚拟化面板 --><ItemsControl.ItemsPanel><ItemsPanelTemplate><VirtualizingStackPanel IsVirtualizing="True"  <!-- 启用虚拟化(默认True) -->VirtualizationMode="Recycling"/> <!-- 优化容器复用 --></ItemsPanelTemplate></ItemsControl.ItemsPanel></ItemsControl>
</ScrollViewer>

最后,当我们想要实现一些自定义的集合控件效果,在重写控件的ItemsPanel的控件模板时,一定不要忘记把ItemsPanel的模板设置为VirtualizingStackPanel,否则,即便我们应用了虚拟化属性。但由于未使用虚拟化控件,也无法开启虚拟化导致重写后的控件模板性能大大降低影响使用。

点击查看代码
<Setter Property="ItemsPanel"><Setter.Value><ItemsPanelTemplate><VirtualizingStackPanelMargin="0"IsItemsHost="True"VirtualizingPanel.IsVirtualizing="True"VirtualizingPanel.VirtualizationMode="Recycling" /></ItemsPanelTemplate></Setter.Value>
</Setter>

防误触

这个非常好理解,当我们点击TreeViewItem时,可能我们不经意间,碰到鼠标左键一滑就直接开启拖拽。从而导致误操作。
优化方向从两方面考虑。

  1. 鼠标点击时距离TreeView边缘的距离。
  2. 当鼠标在按住状态下移动一定距离后,才开启拖拽功能。

思路非常好理解,下面就直接给出对应的代码实现。
在下面的方法中(对于为什么是隧道事件后面会讲),主要完成了两点操作。

  1. 获取选中项
  2. 判断点击位置,如果在合理范围就记录点击位置,然后将拖拽标志设置为True。
PreviewMouseLeftButtonDown
private void ModuleTree_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{if(ModuleTree.Items.Count==0){ModuleTree.Focus();return;}Point pt = e.GetPosition(ModuleTree);HitTestResult result= VisualTreeHelper.HitTest(ModuleTree, pt);if(result == null){return;}TreeViewItem selectedItem = ElementHelper.FindVisualParent<TreeViewItem>(result.VisualHit);if (selectedItem != null) {SelectedNode = selectedItem.DataContext as ModuleNode;selectedItem.IsSelected = true;}//靠近滚轮则不执行拖动if (ModuleTree.ActualWidth - pt.X > 80){if (SelectedNode != null && SelectedNode.IsCategory == false){m_MousePressY = pt.Y;m_MousePressX = pt.X;m_DragModuleName = SelectedNode.Name;m_DragMoveFlag = true;}}
}
在下面的鼠标移动事件中,由于我们先前已经记录了点击的位置,如果移动的幅度超过限制,那我们就开启拖拽操作。
ModuleTree_MouseMove
  private void ModuleTree_MouseMove(object sender, MouseEventArgs e){if(m_DragMoveFlag == true){Point pt = e.GetPosition(ModuleTree);   // 与点击位置超过10个像素,表示能开始拖动if(Math.Abs(pt.Y-m_MousePressY)>10 || Math.Abs(pt.X - m_MousePressX) > 10){string showTxt = SelectedNode.Name;m_DragCursor = CursorHelper.CreateCursor(200,28,12,ImageHelper.ImageSourceToBitmap(SelectedNode.IconImage),26, showTxt);m_DragMoveFlag= false;// 启动拖拽DragDrop.DoDragDrop(ModuleTree,$"{m_DragModuleName}",DragDropEffects.Move);}}}

鼠标拖拽时的样式修改

在拖拽时实现类似于下面Windows的拖拽效果,从而在拖拽过程中实时给予用户反馈。
image

GiveFeedback

实现此类功能,首先需要了解这个事件,GiveFeedback是Drop类的一个附加事件。对拖动源进行拖动时,持续引发 GiveFeedback事件。我们需要在这个事件下定义鼠标指针的修改逻辑,从而实现拖拽过程中的鼠标样式变更。还是先给出代码实现,然后介绍。
下面的代码中,每一步都很重要。在方法中我们首先关闭了光标的默认样式,然后将鼠标设置为了一个新的样式(m_DragCursor是我们在当前类中定义的一个字段,类型为Cursor)。然后设置e.Handled = true;。这步很重要,防止因为冒泡事件导致我们的鼠标样式又被修改回去。

GiveFeedback
private void ModuleTree_GiveFeedback(object sender, GiveFeedbackEventArgs e)
{// 不用启用默认光标e.UseDefaultCursors = false;// 设置鼠标样式Mouse.SetCursor(m_DragCursor);// 防止冒泡事件干扰e.Handled = true;
}

接下来,要回到上面介绍的ModuleTree_MouseMove方法中。在这个方法中,一旦我们开启了拖拽就要去设置 m_DragCursor 为我们指定的鼠标样式。所使用的CreateCursor方法,通过在一个Bitmap上使用Graphics去绘制我们传入的图像以及文本内容,然后通过Bitmap创建出一个Cursor并返回。

点击查看代码
private void ModuleTree_MouseMove(object sender, MouseEventArgs e)
{if(m_DragMoveFlag == true){Point pt = e.GetPosition(ModuleTree);   // 与点击位置超过10个像素,表示能开始拖动if(Math.Abs(pt.Y-m_MousePressY)>10 || Math.Abs(pt.X - m_MousePressX) > 10){string showTxt = SelectedNode.Name;m_DragCursor = CursorHelper.CreateCursor(200,28,12,ImageHelper.ImageSourceToBitmap(SelectedNode.IconImage),26, showTxt);m_DragMoveFlag= false;// 启动拖拽DragDrop.DoDragDrop(ModuleTree,$"{m_DragModuleName}",DragDropEffects.Move);}}
}

高光显示和自动滚动

高光显示

补充

TreeViewItem的事件拦截

在开发TreeView和ListBox等相关功能时,往往需要在集合控件上通过点击事件(MouseLeftButtonDown事件),记录点击位置,获取选中项等操作。
当时,我在写代码时在TreeView使用的是MouseLeftButtonDown,这是一个冒泡事件。然后,我发现当我点击对应的TreeViewItem时,无法触发对应的事件,这个事件只在我点击TreeView的空白部分时才会触发。后来查资料发现,在集合控件中,其本身的Item就已经定义了MouseLeftButtonDown事件(用来修改选中项和变更选中状态等),然后Item会把这个事件截断,导致TreeView无法接收到。
总结一下,就是对于集合控件,想通过鼠标的Down开启拖拽事件,一定要去使用隧道事件PreviewMouseLeftButtonDown。这点很关键而且很细节。

http://www.sczhlp.com/news/536.html

相关文章:

  • 第七天
  • 基于深度学习的YOLO框架的7种交通场景识别项目系统【附完整源码+数据集】
  • 2-2 点灯例程(寄存器开发) - LI,Yi
  • 【Datawhale AI夏令营--task2】科大讯飞AI大赛(大模型技术)
  • 记录一次vue3+mqtt.js连接华为云mqtt的成功经历
  • 狂神说Java|Java基础
  • 每日题单
  • 在常量时间内实现单向链表的插入与删除
  • cpp的单头文件
  • (阶段三:整合)面向用户 面向商户,场景之:shop
  • 现代Web框架的性能基准测试(6084)
  • 服务端推送技术的现代实现(8430)
  • 跨平台Web服务开发的新选择(1992)
  • Astro机器人流畅运动背后的科技原理
  • 实时通信协议的Rust实现(5234)
  • 现代Web框架的性能基准测试(8409)
  • 现代Web服务器性能革命:我的Rust框架探索之旅(1820)
  • 实战项目:文件分块上传系统(4936)
  • HTTP请求处理的高效封装(8307)
  • 实时通信的革命:WebSocket技术的深度探索(1440)
  • Rust生态系统在Web开发中的优势(9219)
  • 高并发处理的Rust实现方案(2866)
  • 从零开始构建高性能实时聊天系统:Hyperlane框架实战指南(5696)
  • 内存使用效率的终极对决:零拷贝技术的实战应用(9040)
  • 推荐6本书《MLIR编译器原理与实践》、《ONNX人工智能技术与开发实践》、《AI芯片开发核心技术详解》、《智能汽车传感器:原理设计应用》、《TVM编译器原理与实践》、《LLVM编译器原理与实践》
  • 实时通信协议的Rust实现(2554)
  • 零依赖Web框架的设计哲学(5850)
  • 微服务架构的轻量级解决方案(8414)
  • WebSocket服务端的高效处理(1857)
  • 利用数据绑定让动画更智能:在Rive中创建动态黄金计算器