.NET `ListView` 控制項雖支援分組 (`Groups` 屬性),但 **預設不提供摺疊/展開功能**。雖然 Vista/Win7+ 原生 ListView 已支援群組狀態 (`LVGS_COLLAPSED`/`LVGS_EXPANDED`),但 .NET Framework 未封裝對應 API。本文透過 P/Invoke 呼叫 `user32.dll` 的 `SendMessage` + `LVM_SETGROUPINFO`/`LVM_HITTEST` 實現完整摺疊展開與點擊群組標題切換。
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public partial class MainForm : Form
{
// 訊息常數
private const int LVM_FIRST = 0x1000;
private const int LVM_SETGROUPINFO = LVM_FIRST + 147;
private const int LVM_HITTEST = LVM_FIRST + 18;
// 群組狀態旗標
[Flags]
private enum GroupState : int
{
COLLAPSIBLE = 8, // 可摺疊 (顯示箭頭)
COLLAPSED = 1, // 已摺疊
EXPANDED = 0 // 已展開
}
// LVGROUP 結構 (對應 LVGROUPW)
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct LVGROUP
{
public int cbSize;
public int mask;
public IntPtr pszHeader;
public int cchHeader;
public IntPtr pszFooter;
public int cchFooter;
public int iGroupId;
public int stateMask;
public int state;
public int uAlign;
}
// LVHITTESTINFO 結構
[StructLayout(LayoutKind.Sequential)]
private struct LVHITTESTINFO
{
public Point pt;
public int flags;
public int iItem;
public int iSubItem;
public int iGroup;
}
// P/Invoke 宣告
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int SendMessage(IntPtr hWnd, int msg, int wParam, ref LVGROUP lParam);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int SendMessage(IntPtr hWnd, int msg, int wParam, ref LVHITTESTINFO lParam);
// ... 其餘實現代碼
}
///
/// 設定指定群組的摺疊狀態
///
/// 群組索引 (0-based)
/// 目標狀態
private void SetGroupState(int groupIndex, GroupState state)
{
LVGROUP group = new LVGROUP
{
cbSize = Marshal.SizeOf(typeof(LVGROUP)),
mask = 4, // LVGF_STATE
state = (int)state,
stateMask = (int)(GroupState.COLLAPSED | GroupState.COLLAPSIBLE),
iGroupId = groupIndex
};
IntPtr ptr = IntPtr.Zero;
try
{
ptr = Marshal.AllocHGlobal(group.cbSize);
Marshal.StructureToPtr(group, ptr, true);
SendMessage(listView1.Handle, LVM_SETGROUPINFO, groupIndex, ref group);
}
finally
{
if (ptr != IntPtr.Zero) Marshal.FreeHGlobal(ptr);
}
}
///
/// 批次設定所有群組為可摺疊狀態 (初始化時呼叫)
///
private void EnableAllGroupsCollapsible()
{
for (int i = 0; i < listView1.Groups.Count; i++)
{
SetGroupState(i, GroupState.COLLAPSIBLE | GroupState.EXPANDED);
listView1.Groups[i].Tag = "EXPANDED"; // 用 Tag 追蹤狀態
}
}
private void ListView1_MouseDown(object sender, MouseEventArgs e)
{
LVHITTESTINFO hitInfo = new LVHITTESTINFO { pt = new Point(e.X, e.Y) };
// 取得點擊位置對應的群組索引
int groupIndex = SendMessage(listView1.Handle, LVM_HITTEST, -1, ref hitInfo);
// flags == 0x50000000 表示點擊在群組標題區域
if (hitInfo.flags == 0x50000000 && groupIndex >= 0 && groupIndex < listView1.Groups.Count)
{
var currentTag = listView1.Groups[groupIndex].Tag?.ToString();
if (currentTag == "EXPANDED")
{
SetGroupState(groupIndex, GroupState.COLLAPSED | GroupState.COLLAPSIBLE);
listView1.Groups[groupIndex].Tag = "COLLAPSED";
}
else if (currentTag == "COLLAPSED")
{
SetGroupState(groupIndex, GroupState.EXPANDED | GroupState.COLLAPSIBLE);
listView1.Groups[groupIndex].Tag = "EXPANDED";
}
}
}
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
listView1.MouseDown += ListView1_MouseDown;
Load += MainForm_Load;
}
private void MainForm_Load(object sender, EventArgs e)
{
// 範例:建立分組與項目
listView1.View = View.Details;
listView1.Columns.Add("Name", 200);
listView1.Columns.Add("Value", 100);
var group1 = listView1.Groups.Add("grp1", "群組一");
var group2 = listView1.Groups.Add("grp2", "群組二");
listView1.Items.Add(new ListViewItem(new[] { "項目 1-1", "值A" }) { Group = group1 });
listView1.Items.Add(new ListViewItem(new[] { "項目 1-2", "值B" }) { Group = group1 });
listView1.Items.Add(new ListViewItem(new[] { "項目 2-1", "值C" }) { Group = group2 });
EnableAllGroupsCollapsible(); // 初始化啟用摺疊
}
// ... 上述 SetGroupState、EnableAllGroupsCollapsible、ListView1_MouseDown 方法
// ... P/Invoke 宣告、結構定義
}
| 技術點 | 說明 |
|---|---|
LVM_SETGROUPINFO | 設定群組資訊 (含狀態),`wParam` 為群組索引,`lParam` 為 `LVGROUP` 結構指標 |
LVGF_STATE` (mask=4) | 告知系統僅更新 `state` 欄位 |
LVGS_COLLAPSIBLE` (8) | **必要旗標**,不設定則無箭頭、不可摺疊 |
LVM_HITTEST` + `0x50000000` | 判斷滑鼠是否點擊在群組標題區域 |
| `Marshal.AllocHGlobal`/`StructureToPtr` | 將 managed 結構封送為 unmanaged 記憶體區塊供 API 讀取 |
| `Group.Tag` 追蹤狀態 | .NET `ListViewGroup` 無狀態屬性,利用 `Tag` 自行維護 |
| 方案 | 優點 | 缺點 |
|---|---|---|
| 本文 P/Invoke 方案 | 原生效能、無第三方依賴、視覺一致 | 代碼複雜、需處理記憶體封送、版本限制 |
| ObjectListView (開源庫) | 功能豐富、支援摺疊、型別安全 | 引入外部依賴、學習成本 |
| TreeView 模擬 | 原生支援摺疊、易用 | 非表格視圖、大量資料效能差 |
| WPF ListView + CollectionView | 原生支援 GroupStyle 摺疊 | 需遷移至 WPF |
透過 P/Invoke 呼叫 `LVM_SETGROUPINFO` 與 `LVM_HITTEST`,配合 `LVGROUP` 結構的 `state` 旗標操作,可在 WinForms `ListView` 中實現原生級的分組摺疊/展開功能。核心在於:
此技術適用於維護舊版 WinForms 專案、無法引入第三方庫、需保持原生外觀的場景。