首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# ListView 分組摺疊/展開完整實現:P/Invoke 呼叫 Windows 原生 API

     編輯:關於C語言

一、背景與挑戰

.NET `ListView` 控制項雖支援分組 (`Groups` 屬性),但 **預設不提供摺疊/展開功能**。雖然 Vista/Win7+ 原生 ListView 已支援群組狀態 (`LVGS_COLLAPSED`/`LVGS_EXPANDED`),但 .NET Framework 未封裝對應 API。本文透過 P/Invoke 呼叫 `user32.dll` 的 `SendMessage` + `LVM_SETGROUPINFO`/`LVM_HITTEST` 實現完整摺疊展開與點擊群組標題切換。

二、核心 Win32 結構與常數定義

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` 自行維護

七、適用版本與限制

  • 適用:Windows Vista / Server 2008 及以上 (包含 Win10/11)。
  • 不適用:Windows XP (原生 ListView v5 不支援群組摺疊)。
  • DPI 感知:高 DPI 下座標計算可能需調整,建議專案設定 `dpiAware`。

八、替代方案比較

方案優點缺點
本文 P/Invoke 方案原生效能、無第三方依賴、視覺一致代碼複雜、需處理記憶體封送、版本限制
ObjectListView (開源庫)功能豐富、支援摺疊、型別安全引入外部依賴、學習成本
TreeView 模擬原生支援摺疊、易用非表格視圖、大量資料效能差
WPF ListView + CollectionView原生支援 GroupStyle 摺疊需遷移至 WPF

九、總結

透過 P/Invoke 呼叫 `LVM_SETGROUPINFO` 與 `LVM_HITTEST`,配合 `LVGROUP` 結構的 `state` 旗標操作,可在 WinForms `ListView` 中實現原生級的分組摺疊/展開功能。核心在於:

  1. 初始化時為所有群組設定 `COLLAPSIBLE | EXPANDED` 狀態。
  2. 攔截 `MouseDown`,透過 `LVM_HITTEST` 判斷是否點擊群組標題。
  3. 切換群組 `Tag` 狀態並呼叫 `SetGroupState` 更新原生控制項。

此技術適用於維護舊版 WinForms 專案、無法引入第三方庫、需保持原生外觀的場景。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved