WPF可拖拽ListView

news2025/7/23 6:16:37
1.控件描述

WPF实现一个ListView控件Item子项可删除也可拖拽排序,效果如下图所示
可拖拽ListView

2.实现代码

配合 WrapPanel 实现水平自动换行,并开启拖拽

<ListView
    x:Name="listView"
    Grid.Row="1"
    Width="300"
    AllowDrop="True"
    Background="#DCE1E7"
    DragEnter="ListView_OnDragEnter"
    DragLeave="ListView_OnDragLeave"
    DragOver="ListView_OnDragOver"
    Drop="ListView_OnDrop"
    FocusVisualStyle="{x:Null}"
    ItemContainerStyle="{StaticResource NoSelectionListViewItemStyle}"
    ItemTemplate="{StaticResource ItemTemplate}"
    ItemsSource="{Binding FilterItems, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
    PreviewKeyDown="ListView_OnPreviewKeyDown"
    PreviewMouseLeftButtonDown="ListView_OnPreviewMouseLeftButtonDown"
    PreviewMouseMove="ListView_OnPreviewMouseMove"
    SelectionChanged="ListView_OnSelectionChanged">
    <ListView.Resources>
        <Style TargetType="ScrollViewer">
            <Setter Property="HorizontalScrollBarVisibility" Value="Disabled" />
            <Setter Property="VerticalScrollBarVisibility" Value="Auto" />
        </Style>
    </ListView.Resources>
    <ListView.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel />
        </ItemsPanelTemplate>
    </ListView.ItemsPanel>
</ListView>

模版及样式,引用图片自行替换

<Style x:Key="deleteImgStyle" TargetType="{x:Type Image}">
    <Setter Property="Width" Value="16" />
    <Setter Property="Height" Value="16" />
    <Style.Triggers>
        <Trigger Property="IsMouseOver" Value="True">
            <Setter Property="Opacity" Value="0.8" />
        </Trigger>
    </Style.Triggers>
</Style>
<Style x:Key="NoSelectionListViewItemStyle" TargetType="ListViewItem">
    <Setter Property="Background" Value="Transparent" />
    <Setter Property="FocusVisualStyle" Value="{x:Null}" />
    <Setter Property="IsSelected" Value="{Binding IsSelected}" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ListViewItem">
                <Grid x:Name="Grid" Background="{TemplateBinding Background}">
                    <ContentPresenter />
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsSelected" Value="True">
                        <Setter TargetName="Grid" Property="Background" Value="Transparent" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
<DataTemplate x:Key="ItemTemplate" DataType="local:FilterItem">
    <Border
        x:Name="border"
        Height="30"
        Margin="2,2,3,3"
        HorizontalAlignment="Stretch"
        Background="#f7f7f8"
        CornerRadius="5"
        Cursor="Hand">
        <Grid
            x:Name="innerGrid"
            MinWidth="20"
            VerticalAlignment="Center">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <Label
                x:Name="label"
                Margin="2,0,0,0"
                Content="{Binding DisplayText}"
                FontSize="13"
                Foreground="#000" />
            <Image
                Grid.Column="1"
                Margin="5,0"
                MouseLeftButtonDown="Image_MouseLeftButtonDown"
                Source="pack://application:,,,/WPFTest;component/Resources/delete.png"
                Stretch="Uniform"
                Style="{StaticResource deleteImgStyle}"
                Tag="{Binding}" />
        </Grid>
    </Border>
    <DataTemplate.Triggers>
        <Trigger SourceName="border" Property="IsMouseOver" Value="True">
            <Setter TargetName="border" Property="Background" Value="#80f7f7f8" />
        </Trigger>
        <DataTrigger Binding="{Binding IsSelected}" Value="True">
            <Setter TargetName="border" Property="Background" Value="#BCC2C9" />
        </DataTrigger>
        <DataTrigger Binding="{Binding IsDraggedOver}" Value="True">
            <Setter TargetName="border" Property="Background" Value="DarkOrange" />
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>

后台代码

private ObservableCollection<FilterItem> _filterItems;
/// <summary>
/// 绑定数据源
/// </summary>
public ObservableCollection<FilterItem> FilterItems
{
    get => _filterItems;
    set { _filterItems = value; OnPropertyChanged(nameof(FilterItems)); }
}

// 删除Item
private void Image_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    e.Handled = true;
    if (sender is Image image)
    {
        if (image.Tag is FilterItem item)
        {
            FilterItems.Remove(item);
        }
    }
}

// 键盘方向键实现Item排序
private void ListView_OnPreviewKeyDown(object sender, KeyEventArgs e)
{
    if (FilterItems?.Count > 1 && listView.SelectedIndex >= 0)
    {
        var selIndex = listView.SelectedIndex;
        var item = FilterItems[selIndex];
        if (e.Key == Key.Left && selIndex > 0)
        {
            FilterItems.RemoveAt(selIndex);
            FilterItems.Insert(selIndex - 1, item);
        }
        else if (e.Key == Key.Right && selIndex < FilterItems.Count - 1)
        {
            FilterItems.RemoveAt(selIndex);
            FilterItems.Insert(selIndex + 1, item);
        }
    }
    e.Handled = true;
}

#region 拖拽排序
private ListViewItem _draggedItem;  // 用于存储被拖动的项
private ListViewItem _dropTargetItem;  // 用于存储当前的拖拽目标项
// 标记是否处于拖拽状态
private bool _isDragging;
// 鼠标按下时的位置
private Point _mouseDownPosition;
// 最小拖拽距离
private const double DragThreshold = 10.0;

/// <summary>
/// 处理拖动开始的操作
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ListView_OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    var item = FindVisualParent<ListViewItem>(e.OriginalSource as DependencyObject);
    if (item != null)
    {
        _draggedItem = item;
        _mouseDownPosition = e.GetPosition(null);
    }
}

private void ListView_OnPreviewMouseMove(object sender, MouseEventArgs e)
{
    if (_draggedItem != null && e.LeftButton == MouseButtonState.Pressed)
    {
        var currentPosition = e.GetPosition(null);
        if (Math.Abs(currentPosition.X - _mouseDownPosition.X) > DragThreshold ||
            Math.Abs(currentPosition.Y - _mouseDownPosition.Y) > DragThreshold)
        {
            _isDragging = true;
            // 开始拖动操作
            DragDrop.DoDragDrop(_draggedItem, _draggedItem.Content, DragDropEffects.Move);
            _isDragging = false;
            _draggedItem = null;  // 清除拖拽项
        }
    }
}

/// <summary>
/// 处理拖动过程中的操作
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ListView_OnDragOver(object sender, DragEventArgs e)
{
    var listView = sender as ListView;
    var point = e.GetPosition(listView);
    var hitTestResult = VisualTreeHelper.HitTest(listView, point);
    if (hitTestResult != null)
    {
        var targetItem = FindVisualParent<ListViewItem>(hitTestResult.VisualHit);
        if (targetItem != null && targetItem != _draggedItem)
        {
            // 只有在拖拽目标项发生变化时才进行更新
            if (_dropTargetItem != targetItem)
            {
                // 恢复之前目标项的背景色
                if (_dropTargetItem != null)
                {
                    if (_dropTargetItem.DataContext is FilterItem previousViewModel)
                    {
                        previousViewModel.IsDraggedOver = false;
                    }
                }

                // 高亮显示当前拖拽目标项
                if (targetItem.DataContext is FilterItem targetViewModel)
                {
                    targetViewModel.IsDraggedOver = true;
                }

                _dropTargetItem = targetItem;
            }
            e.Effects = DragDropEffects.Move;  // 允许移动操作
            e.Handled = true;
        }
    }
}

private void ListView_OnDragEnter(object sender, DragEventArgs e)
{
    // 设置拖拽目标项的状态为被拖拽
    var listView = sender as ListView;
    var point = e.GetPosition(listView);
    var hitTestResult = VisualTreeHelper.HitTest(listView, point);
    if (hitTestResult != null)
    {
        var targetItem = FindVisualParent<ListViewItem>(hitTestResult.VisualHit);
        if (targetItem != null && targetItem != _draggedItem)
        {
            // 更新之前拖拽目标项的状态
            if (_dropTargetItem != null && _dropTargetItem != targetItem)
            {
                if (_dropTargetItem.DataContext is FilterItem previousViewModel)
                {
                    previousViewModel.IsDraggedOver = false;
                }
            }

            // 更新当前拖拽目标项的状态
            if (targetItem.DataContext is FilterItem currentViewModel)
            {
                currentViewModel.IsDraggedOver = true;
            }

            _dropTargetItem = targetItem;
        }
    }
    e.Effects = DragDropEffects.Move;  // 允许移动操作
    e.Handled = true;  // 标记事件已处理
}

/// <summary>
/// 处理拖动离开目标区域的操作
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ListView_OnDragLeave(object sender, DragEventArgs e)
{
    // 恢复目标项的背景色
    if (_dropTargetItem != null)
    {
        if (_dropTargetItem.DataContext is FilterItem viewModel)
        {
            viewModel.IsDraggedOver = false;
        }
        _dropTargetItem = null;  // 清除拖拽目标项
    }
}

/// <summary>
/// 处理放置操作
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ListView_OnDrop(object sender, DragEventArgs e)
{
    // 检查拖动项和目标项是否有效
    if (_draggedItem != null && _dropTargetItem != null && _draggedItem != _dropTargetItem)
    {
        var items = listView.Items.OfType<object>().ToList();
        var draggedIndex = items.IndexOf(_draggedItem.Content);
        var dropIndex = items.IndexOf(_dropTargetItem.Content);

        if (draggedIndex != -1 && dropIndex != -1)
        {
            // 移动项的位置
            var item = FilterItems[draggedIndex];
            FilterItems.RemoveAt(draggedIndex);
            FilterItems.Insert(dropIndex, item);

            // 恢复目标项的背景色
            if (_dropTargetItem.DataContext is FilterItem dropViewModel)
            {
                dropViewModel.IsDraggedOver = false;
            }
        }

        // 清除拖拽项和目标项的引用
        _draggedItem = null;
        _dropTargetItem = null;
    }
    else
    {
        // 处理无效的放置操作
        if (_dropTargetItem != null)
        {
            if (_dropTargetItem.DataContext is FilterItem dropViewModel)
            {
                dropViewModel.IsDraggedOver = false;
            }
        }

        _draggedItem = null;
        _dropTargetItem = null;
    }
}
// 查找可视树中的父级项
private T FindVisualParent<T>(DependencyObject child) where T : DependencyObject
{
    while (child != null && !(child is T))
    {
        child = VisualTreeHelper.GetParent(child);
    }
    return child as T;
}
#endregion

绑定项实体类

public class FilterItem : INotifyPropertyChanged
{
    private string _displayText;
    public string DisplayText
    {
        get => _displayText;
        set { _displayText = value; OnPropertyChanged(nameof(DisplayText)); }
    }

    private bool _isSelected;
    public bool IsSelected
    {
        get => _isSelected;
        set { _isSelected = value; OnPropertyChanged(nameof(IsSelected)); }
    }

    private bool _isDraggedOver;
    public bool IsDraggedOver
    {
        get => _isDraggedOver;
        set
        {
            if (_isDraggedOver != value)
            {
                _isDraggedOver = value;
                OnPropertyChanged(nameof(IsDraggedOver));
            }
        }
    }

    public FilterItem()
    { }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}

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

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

相关文章

[蓝桥杯]倍数问题

倍数问题 题目描述 众所周知&#xff0c;小葱同学擅长计算&#xff0c;尤其擅长计算一个数是否是另外一个数的倍数。但小葱只擅长两个数的情况&#xff0c;当有很多个数之后就会比较苦恼。现在小葱给了你 nn 个数&#xff0c;希望你从这 nn 个数中找到三个数&#xff0c;使得…

【MySQL】 约束

一、约束的定义 MySQL 约束是用于限制表中数据的规则&#xff0c;确保数据的 准确性 和 一致性 。约束可以在创建表时定义&#xff0c;也可以在表创建后通过修改表结构添加。 二、常见的约束类型 2.1 NOT NULL 非空约束 加了非空约束的列不能为 NULL 值&#xff0c;如果可以…

前端vue打开多个窗口,关闭窗口后才继续执行后续逻辑

1.打开第一个弹窗 弹窗的按钮代码 2.点击窗口1中按钮&#xff0c;打开新的窗口 // 请领单按钮点击 async cb_6_delClick() {let ls_yfbm this.st_3Value.BMBMlet pstring {}pstring.a ls_yfbmpstring.b this.queryFormDialog.outDepotDeptCodeawait this.openwithparm_w_md…

「深度拆解」Spring Boot如何用DeepSeek重构MCP通信层?从线程模型到分布式推理的架构进化

什么是MCP&#xff1f; MCP&#xff08;Model Context Protocol&#xff0c;模型上下文协议&#xff09;是由Anthropic公司于2024年11月推出的开放标准协议&#xff0c;旨在为大型语言模型&#xff08;LLM&#xff09;与外部数据源、工具及系统提供统一的交互接口&#xff0c;被…

【西门子杯工业嵌入式-2-点亮一颗LED】

西门子杯工业嵌入式-2-点亮一颗LED 一、课程回顾与目标1.上节课内容回顾2.本节课目标 二、硬件连接与原理1. 硬件连接方式2. 连接实例 三、GPIO原理知识1. GPIO结构2. 推挽输出模式原理 四、软件实现步骤1. 项目结构设置2. 函数定义3. led.c 文件编写初始化函数 led_init交替闪…

代码随想录算法训练营第60期第五十五天打卡

大家好&#xff0c;我们今天继续我们图论的部分&#xff0c;其实我们昨天是主要讲解了深搜与广搜的理论基础&#xff0c;我们大体上了解了两种算法的差异与适用情景&#xff0c;今天我们就继续我们的图论的章节&#xff0c;以后几天的题目是图论中比较有名的问题叫做岛屿问题&a…

重磅更新! 基于Gemini 2.5 Pro打造的AI智能体PlantUML-X上线!

目录 图表绘制AI智能体PlantUML-X上线通过简单的提示词创建各种UML图&#xff1a;轻松搞定其它类型的技术图表&#xff1a; AI智能体PlantUML-X功能实测画一个在Java中的一个简单的用户登录功能的时序图效果展示&#xff1a;根据详细内容生成系统架构图效果展示&#xff1a;效果…

[5-02-04].第01节:Jmeter环境搭建:

JMeter笔记大纲 Jmeter依赖于JDK&#xff0c;所以必须确保当前计算机上已经安装了JDK&#xff0c;并且配置了环境变量 一、JMeter概述&#xff1a; 1.1.JMeter是什么&#xff1a; JMeter是Appache组织使用java开发的一款测试工具 可以用于对服务器、网络或对象模拟巨大的负载…

AI智能推荐实战之RunnableParallel并行链

导读&#xff1a;在现代AI应用开发中&#xff0c;如何高效处理多维度数据分析始终是开发者面临的核心挑战。当您需要同时进行情感分析、关键词提取和实体识别&#xff0c;或者要对比多个AI模型的输出结果时&#xff0c;传统的串行处理方式往往效率低下。 本文将深入解析LangCha…

Gemini开源项目DeepResearch:基于LangGraph的智能研究代理技术原理与实现

引言 在人工智能快速发展的今天&#xff0c;如何构建一个能够进行深度研究、自主学习和迭代优化的AI系统成为了技术前沿的重要课题。Gemini开源的DeepResearch一周收获7.9k Star&#xff0c;Google的开源项目Gemini DeepResearch技术通过结合LangGraph框架和Gemini大语言模型&…

Doris Catalog 联邦分析查询性能优化:从排查到优化的完整指南

在大数据分析中&#xff0c;Doris 的 Catalog 联邦分析功能为整合多源数据提供了有力支持。然而&#xff0c;在实际应用中&#xff0c;可能会遇到各种问题影响其正常运行。本文将详细剖析这些问题并提供解决方案。 一、联邦分析查询慢&#xff1a;内外表通用排查逻辑 当遇到 …

01 Deep learning神经网络的编程基础 二分类--吴恩达

二分类 1. 核心定义 二分类任务是监督学习中最基础的问题类型&#xff0c;其目标是将样本划分为两个互斥类别。设样本特征空间为 X ⊆ R n \mathcal{X} \subseteq \mathbb{R}^n X⊆Rn&#xff0c;输出空间为 Y { 0 , 1 } \mathcal{Y} \{0,1\} Y{0,1}&#xff0c;学习目标为…

视频自动化分割方案:支持按时间与段数拆分

在日常视频处理任务中&#xff0c;如何快速将一个较长的视频文件按照指定规则拆分为多个片段&#xff0c;是许多用户都会遇到的问题。尤其对于需要批量处理视频的开发者、自媒体运营者或内容创作者来说&#xff0c;手动剪辑不仅效率低下&#xff0c;还容易出错。这是一款绿色免…

股指期货合约价值怎么算?

股指期货合约价值就是你买一手股指期货合约&#xff0c;理论上值多少钱。这个价值是根据期货的价格和合约乘数来计算的。就好比你买了一斤苹果&#xff0c;价格是5块钱一斤&#xff0c;那你买一斤就得付5块钱。股指期货也是一样&#xff0c;只不过它的计算稍微复杂一点点。 一…

【QT】使用QT帮助手册找控件样式

选择帮助—》输入stylesheet(小写)—》选择stylesheet—》右侧选择Qt Style Sheets Reference 2.使用CtrlF—》输入要搜索的控件—》点击Customizing QScrollBar 3.显示参考样式表–》即可放入QT-designer的样式表中

计算机网络(5)——数据链路层

1.概述 数据链路层负责一套链路上从一个节点向另一个物理链路直接相连的相邻节点传输数据报。换言之&#xff0c;主要解决相邻节点间的可靠数据传输 节点(nodes)&#xff1a;路由器和主机 链路(links)&#xff1a;连接相邻节点的通信信道 2.数据链路层服务 2.1 组帧 组帧(fra…

VuePress完美整合Toast消息提示

VuePress 整合 Vue-Toastification 插件笔记 记录如何在 VuePress 项目中整合使用 vue-toastification 插件&#xff0c;实现优雅的消息提示。 一、安装依赖 npm install vue-toastification或者使用 yarn&#xff1a; yarn add vue-toastification二、配置 VuePress 客户端增…

adb 连不上真机设备问题汇总

问题一、无法弹出 adb 调试授权弹窗 详细描述&#xff1a; 开发者选项中已打开 usb 调试&#xff0c;仅充电模式下 usb 调试也已打开&#xff0c;电脑通过 usb 连上手机后&#xff0c;一直弹出 adb 调试授权弹窗&#xff0c;尝试取消授权再次连接&#xff0c;还是无法弹出问题…

[yolov11改进系列]基于yolov11引入注意力机制SENetV1或者SENetV2的python源码+训练源码

本文给大家带来的改进机制是SENet&#xff08;Squeeze-and-Excitation Networks&#xff09;其是一种通过调整卷积网络中的通道关系来提升性能的网络结构。SENet并不是一个独立的网络模型Q&#xff0c;而是一个可以和现有的任何一个模型相结合的模块&#xff08;可以看作是一种…

鸿蒙仓颉语言开发实战教程:商城搜索页

大家好&#xff0c;今天要分享的是仓颉语言商城应用的搜索页。 搜索页的内容比较多&#xff0c;都有点密集恐惧症了&#xff0c;不过我们可以从上至下将它拆分开来&#xff0c;逐一击破。 导航栏 搜索页的的最顶部是导航栏&#xff0c;由返回按钮和搜索框两部分组成,比较简单…