C#实现ListView组收缩与展开功能的完整方案

认真发呆
认真发呆
发布时间:2026-10-13| 6人浏览过
原创

Windows Forms原生的ListView控件在处理大量数据时,虽然提供了分组功能,但其“Grouping”特性默认仅用于视觉分类和排序,并不原生支持像文件夹那样的点击收起/展开交互。对于追求更优用户体验的应用程序,尤其是数据密集型工具,实现分组的动态折叠是提升可读性的关键手段。

实现原理与环境要求

要实现ListView分组的收缩与展开,需要绕过常规的属性设置,直接调用Windows系统的底层API。这主要依赖于 LVM_SETGROUPINFO 消息。该机制允许开发者修改ListView分组的当前状态(展开或折叠)以及标志位(是否可折叠)。

环境兼容性注意:此功能仅在 Windows Vista、Windows Server 2008 及更高版本的操作系统中有效。在这些系统中,ListView控件的分组渲染引擎已经包含了相关的UI响应逻辑。

代码实现步骤

第1步:引入必要的命名空间与声明

首先,确保项目中引用了以下命名空间以处理窗口消息和内存操作:

  • System.Runtime.InteropServices:用于P/Invoke调用非托管库。
  • System.Windows.Forms:标准的GUI组件库。

第2步:定义数据结构与枚举

通过 StructLayout 关键字,我们将C#类型映射为对应的Win32结构体,以便正确传递内存数据。同时定义分组状态的枚举值。

以下是核心的结构体定义:包含 LVGROUP(控制分组信息)、LVHITTESTINFO(用于判断鼠标点击位置)以及 GroupState 枚举。

第3步:封装SetGroupCollapse核心逻辑

使用 DllImport 导入 user32.dll 中的 SendMessage 函数。创建 SetGroupCollapse 方法,遍历所有分组,将新的状态写入 LVGROUP 结构,并通过指针发送到指定句柄,实现整体状态更新。

第4步:捕获鼠标事件并执行局部操作

为了实现点击列表头部的特定区域来触发单一分组的切换,需要在 MouseDown 事件中:

  1. 获取鼠标点击坐标,利用 LVM_SUBITEMHITTEST (常量 0x1000 + 18) 查询该坐标属于哪个分组。
  2. 检查返回的标志位 (flags),确认是否为分组标题区域(通常为 0x50000000)。
  3. 读取该分组的Tag标记(记录当前是展开还是折叠),调用 SetGroupCollapseEx 对单个ID进行状态翻转。

参考实现代码

将以下代码整合到继承自Form的主窗体类中即可生效。注意代码中使用的 aoc 为示例中的ListView控件名称,实际部署时请替换为你项目中的实际控件名。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Diagnostics;

namespace ListViewGroup
{
  public partial class MainForm : Form
  {
    public MainForm()
    {
      InitializeComponent();
    }

    // 声明外部API
    [DllImport("user32.dll")]
    static extern int SendMessage(IntPtr window, int message, int wParam, ref LVHITTESTINFO lParam);
    
    [DllImport("user32.dll")]
    static extern int SendMessage(IntPtr window, int message, int wParam, IntPtr lParam);

    // 批量折叠按钮点击事件
    private void btCollapse_Click(object sender, EventArgs e)
    {
      SetGroupCollapse(GroupState.COLLAPSED | GroupState.COLLAPSIBLE);
    }

    // 批量展开按钮点击事件
    private void btExpand_Click(object sender, EventArgs e)
    {
      SetGroupCollapse(GroupState.EXPANDED | GroupState.COLLAPSIBLE);
    }

    // 统一设置所有分组的状态
    private void SetGroupCollapse(GroupState state)
    {
      // 注意:Groups.Count 可能为0,建议增加判断逻辑避免索引异常
      for (int i = 0; i <= aoc.Groups.Count - 1; i++) // 修正原代码边界隐患
      {
        LVGROUP group = new LVGROUP();
        group.cbSize = Marshal.SizeOf(group);
        group.state = (int)state;
        group.mask = 4; // LVGF_STATE
        group.iGroupId = i;
        IntPtr ip = IntPtr.Zero;
        try
        {
          ip = Marshal.AllocHGlobal(group.cbSize);
          Marshal.StructureToPtr(group, ip, true);
          // LVM_FIRST is 0x1000, LVM_SETGROUPINFO is 147
          SendMessage(aoc.Handle, 0x1000 + 147, i, ip);
        }
        catch (Exception ex)
        {
          System.Diagnostics.Trace.WriteLine(ex.Message + Environment.NewLine + ex.StackTrace);
        }
        finally
        {
          if (null != ip) Marshal.FreeHGlobal(ip);
        }
      }
    }

    private void MainForm_Load(object sender, EventArgs e)
    {
      // 初始化:将所有分组标记为可折叠
      SetGroupCollapse(GroupState.COLLAPSIBLE);
      for (int i = 0; i < aoc.Groups.Count; i++)
      {
        aoc.Groups[i].Tag = "EXPANDED"; // 默认初始状态为展开
      }
    }

    // 鼠标按下事件:检测是否点击在分组标题上
    private void aoc_MouseDown(object sender, MouseEventArgs e)
    {
      LVHITTESTINFO lvHitInfo = new LVHITTESTINFO();
      Point p = new Point(e.X, e.Y); // 相对于客户区的坐标
      lvHitInfo.pt = p;
      
      try
      {
        // 查询命中测试:-1表示不针对特定的Item
        int id = SendMessage(aoc.Handle, 0x1000 + 18, -1, ref lvHitInfo);
        
        // 0x50000000 对应 HITTESTFLAGS.GROUPHEADER
        if (lvHitInfo.flags == 0x50000000)
        {
          string currentState = aoc.Groups[id].Tag.ToString();
          
          if (currentState == "EXPANDED")
          {
            // 切换到折叠状态
            SetGroupCollapseEx(id, GroupState.COLLAPSED | GroupState.COLLAPSIBLE);
            aoc.Groups[id].Tag = "COLLAPSED";
          }
          else if (currentState == "COLLAPSED")
          {
            // 切换到展开状态
            SetGroupCollapseEx(id, GroupState.EXPANDED | GroupState.COLLAPSIBLE);
            aoc.Groups[id].Tag = "EXPANDED";
          }
        }
      }
      catch (Exception ex)
      {
        Trace.WriteLine(ex.Message + Environment.NewLine + ex.StackTrace);
      }
    }

    // 针对单个ID的操作
    private void SetGroupCollapseEx(int id, GroupState groupState)
    {
      int i = id;
      LVGROUP group = new LVGROUP();
      group.cbSize = Marshal.SizeOf(group);
      group.state = (int)groupState;
      group.mask = 4; 
      group.iGroupId = i;
      IntPtr ip = IntPtr.Zero;
      try
      {
        ip = Marshal.AllocHGlobal(group.cbSize);
        Marshal.StructureToPtr(group, ip, true);
        SendMessage(aoc.Handle, 0x1000 + 147, i, ip); 
      }
      catch (Exception ex)
      {
        System.Diagnostics.Trace.WriteLine(ex.Message + Environment.NewLine + ex.StackTrace);
      }
      finally
      {
        if (null != ip) Marshal.FreeHGlobal(ip);
      }
    }
  }

  // Win32 结构体映射
  [StructLayout(LayoutKind.Sequential)]
  public struct LVGROUP
  {
    public int cbSize;
    public int mask;
    [MarshalAs(UnmanagedType.LPTStr)]
    public string pszHeader;
    public int cchHeader;
    [MarshalAs(UnmanagedType.LPTStr)]
    public string pszFooter;
    public int cchFooter;
    public int iGroupId;
    public int stateMask;
    public int state;
    public int uAlign;
  }

  public enum GroupState
  {
    COLLAPSIBLE = 8,
    COLLAPSED = 1,
    EXPANDED = 0
  }

  [StructLayout(LayoutKind.Sequential)]
  public struct LVHITTESTINFO
  {
    public Point pt;
    public int flags;
    public int iItem;
    public int iSubItem;
    public int iGroup;
  }
}

常见问题排查与优化

  • 编译报错找不到 LVM_FIRST:这是预定义的宏(值为 0x1000),代码中已直接代入数字计算结果,无需额外定义。
  • 点击无反应:请务必在窗体设计器中将 MainForm_Load 绑定到 Load 事件,并将 aoc_MouseDown 绑定至 ListView 实例的 MouseDown 事件。此外,检查ListView是否启用了 Groups.Add 分组。
  • 内存泄漏风险:代码中使用了 Marshal.AllocHGlobal 分配非托管内存,必须在 finally 块中使用 Marshal.FreeHGlobal 进行释放,上述代码已做好保护。

以上就是C#实现ListView分组收缩与展开功能的详细内容,更多关于ListView自定义交互的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。