potisanのプログラミングメモ

趣味のプログラマーがプログラミング関係で気になったことや調べたことをいつでも忘れられるようにメモするブログです。はてなブログ無料版なので記事の上の方はたぶん広告です。記事中にも広告挿入されるみたいです。

C#&Win32 API ソートマークやスプリットボタンに対応したリストビューコントロール(ListViewEx)

リストビュー(ListView)から派生してソートマークやスプリットボタン(ドロップダウン用)に対応したリストビューコントロールのコードです。

Program.cs

using System;
using System.Drawing;
using System.Windows.Forms;

static class Program
{
    [STAThread]
    static void Main()
    {
        Application.SetHighDpiMode(HighDpiMode.SystemAware);
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        var form = new Form();
        var listView1 = new ListViewEx();
        listView1.View = View.Details;
        listView1.Dock = DockStyle.Fill;
        listView1.Columns.Add("Column0");
        listView1.Columns.Add("Column1");
        listView1.Columns.Add("Column2");
        form.Controls.Add(listView1);

        listView1.SetColumnSortDown(0, true);
        listView1.SetColumnSortUp(1, true);
        listView1.SetColumnSplitButton(2, true);
        // 1番目のカラムはソートマークの切り替え対応
        listView1.ColumnClick += (sender, e) =>
        {
            if (e.Column == 0)
            {
                if (listView1.GetColumnSortDown(0))
                {
                    listView1.SetColumnSortDown(0, false);
                    listView1.SetColumnSortUp(0, true);
                }
                else
                {
                    listView1.SetColumnSortDown(0, true);
                    listView1.SetColumnSortUp(0, false);
                }
            }
        };
        // 3番目のカラムはドロップダウン(スプリットボタンのクリック)に対応
        listView1.ColumnDropDown += (sender, e) =>
        {
            if (e.Column == 2)
            {
                var columnBounds = form.RectangleToClient(
                    listView1.RectangleToScreen(listView1.GetColumnBounds(2)));

                var panel = new Panel();
                panel.Size = columnBounds.Size;
                panel.Location = new Point(columnBounds.Left, columnBounds.Bottom);
                panel.Capture = true;
                panel.MouseDown += (sender, e) => form.Controls.Remove(panel);
                form.Controls.Add(panel);
                form.Controls.SetChildIndex(panel, 0); // 一番上に移動
            }
        };

        // カラムサイズの調整
        // そのまま表示するとスプリットボタンが隠れる。
        listView1.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize);

        Application.Run(form);
    }
}

ListViewEx.cs

using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.ComponentModel;

class ListViewEx : ListView
{
    [Category("Action")]
    public event EventHandler<ListViewColumnDropDownEventArgs> ColumnDropDown;

    public ListViewEx()
        : base()
    {
    }

    public IntPtr HeaderWindowHandle
    {
        get => NativeMethods.SendMessage(Handle, Constants.LVM_GETHEADER, 0, 0);
    }

    private int GetFormat(int column)
    {
        if (!(0 <= column && column < Columns.Count))
            throw new ArgumentOutOfRangeException(nameof(column));

        HD_ITEMW item = default;
        item.mask = Constants.HDI_FORMAT;
        var ret = NativeMethods.SendMessage(
            HeaderWindowHandle,
            Constants.HDM_GETITEMW, column, ref item);
        if (ret == 0)
            Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
        return item.fmt;
    }

    private void SetFormat(int column, int format)
    {
        if (!(0 <= column && column < Columns.Count))
            throw new ArgumentOutOfRangeException(nameof(column));

        HD_ITEMW item = default;
        item.mask = Constants.HDI_FORMAT;
        item.fmt = format;
        var ret = NativeMethods.SendMessage(
            HeaderWindowHandle,
            Constants.HDM_SETITEMW, column, ref item);
        if (ret == 0)
            Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
    }

    private static bool HasFlag(int flags, int flag) => (flags & flag) == flag;
    private static int SetFlags(int flags, int flag, bool on)
    {
        if (on)
            flags |= flag;
        else
            flags &= ~flag;
        return flags;
    }

    public bool GetColumnSortUp(int column)
        => HasFlag(GetFormat(column), Constants.HDF_SORTUP);

    public void SetColumnSortUp(int column, bool flag)
        => SetFormat(column, SetFlags(GetFormat(column), Constants.HDF_SORTUP, flag));

    public bool GetColumnSortDown(int column)
        => HasFlag(GetFormat(column), Constants.HDF_SORTDOWN);

    public void SetColumnSortDown(int column, bool flag)
        => SetFormat(column, SetFlags(GetFormat(column), Constants.HDF_SORTDOWN, flag));

    public bool GetColumnFixedWidth(int column)
        => HasFlag(GetFormat(column), Constants.HDF_FIXEDWIDTH);

    public void SetColumnFixedWidth(int column, bool flag)
        => SetFormat(column, SetFlags(GetFormat(column), Constants.HDF_FIXEDWIDTH, flag));

    public bool GetColumnSplitButton(int column)
    {
        ThrowIfParentIsNull();
        return HasFlag(GetFormat(column), Constants.HDF_SPLITBUTTON);
    }

    public void SetColumnSplitButton(int column, bool flag)
    {
        ThrowIfParentIsNull();
        SetFormat(column, SetFlags(GetFormat(column), Constants.HDF_SPLITBUTTON, flag));
    }

    public Rectangle GetColumnBounds(int column)
    {
        if (!(0 <= column && column < Columns.Count))
            throw new ArgumentOutOfRangeException(nameof(column));

        var ret = NativeMethods.SendMessage(
            HeaderWindowHandle, Constants.HDM_GETITEMRECT, column, out var rc);
        if (ret == 0)
            Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
        return rc.ToRectangle();
    }

    public Rectangle GetDropDownColumnBounds(int column)
    {
        if (!(0 <= column && column < Columns.Count))
            throw new ArgumentOutOfRangeException(nameof(column));

        var ret = NativeMethods.SendMessage(
            HeaderWindowHandle, Constants.HDM_GETITEMDROPDOWNRECT, column, out var rc);
        if (ret == 0)
            Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
        return rc.ToRectangle();
    }

    private void ThrowIfParentIsNull()
    {
        if (Parent == null)
            throw new InvalidOperationException("この操作はコンテナへの追加後に実行してください。");
    }

    public class ListViewColumnDropDownEventArgs : EventArgs
    {
        public int Column;
    }

    protected override void WndProc(ref Message m)
    {
        if (m.Msg != Constants.WM_REFLECT + Constants.WM_NOTIFY)
        {
            base.WndProc(ref m);
            return;
        }

        var nmlv = Marshal.PtrToStructure<NMLISTVIEW>(m.LParam);
        if (nmlv.hdr.code != Constants.LVN_COLUMNDROPDOWN)
        {
            base.WndProc(ref m);
            return;
        }

        var e = new ListViewColumnDropDownEventArgs();
        e.Column = nmlv.iSubItem;
        ColumnDropDown?.Invoke(this, e);
    }

    private static class Constants
    {
        public const int LVM_FIRST = 0x1000;
        public const int LVM_GETHEADER = LVM_FIRST + 31;

        public const int HDM_FIRST = 0x1200;
        public const int HDM_GETITEMRECT = HDM_FIRST + 7;
        public const int HDM_GETITEMW = HDM_FIRST + 11;
        public const int HDM_SETITEMW = HDM_FIRST + 12;
        public const int HDM_GETITEMDROPDOWNRECT = HDM_FIRST + 25;

        public const uint HDI_FORMAT = 0x0004;

        public const int HDF_SORTUP = 0x0400;
        public const int HDF_SORTDOWN = 0x0200;
        public const int HDF_FIXEDWIDTH = 0x0100;
        public const int HDF_SPLITBUTTON = 0x1000000;

        public const int WM_NOTIFY = 0x004E;
        public const int WM_REFLECT = 0x2000;

        public const uint LVN_COLUMNDROPDOWN = unchecked((uint)(-100 - 64));
    }

    private static class NativeMethods
    {
        [DllImport("user32.dll")]
        public static extern nint SendMessage(IntPtr hWnd, uint Msg, nint wParam, nint lParam);
        [DllImport("user32.dll")]
        public static extern nint SendMessage(IntPtr hWnd, uint Msg, nint wParam, ref HD_ITEMW lParam);
        [DllImport("user32.dll")]
        public static extern nint SendMessage(IntPtr hWnd, uint Msg, nint wParam, out RECT lParam);
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct RECT
    {
        public int Left;
        public int Top;
        public int Right;
        public int Bottom;

        public Rectangle ToRectangle()
        {
            return Rectangle.FromLTRB(Left, Top, Right, Bottom);
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct HD_ITEMW
    {
        public uint mask;
        public int cxy;
        public IntPtr pszText;
        public IntPtr hbm;
        public int cchTextMax;
        public int fmt;
        public nint lParam;
        public int iImage;
        public int iOrder;
        public IntPtr type;
        public IntPtr pvFilter;
        public uint state;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct NMHDR
    {
        public IntPtr hwndFrom;
        public nuint idFrom;
        public uint code;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct NMLISTVIEW
    {
        public NMHDR hdr;
        public int iItem;
        public int iSubItem;
        public uint uNewState;
        public uint uOldState;
        public uint uChanged;
        public Point ptAction;
        public IntPtr lParam;
    }
}

関連記事