C#实现ListView组收缩与展开功能的完整方案
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 事件中:
- 获取鼠标点击坐标,利用
LVM_SUBITEMHITTEST(常量 0x1000 + 18) 查询该坐标属于哪个分组。 - 检查返回的标志位 (
flags),确认是否为分组标题区域(通常为0x50000000)。 - 读取该分组的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自定义交互的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









