potisanのプログラミングメモ

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

C#10&Win32 API リストビューのヘッダーにソートマークやドロップダウン(スプリットボタン)を表示する

概要

f:id:potisan:20210306214832p:plain

標準コントロールのリストビューはWin32 APIのリストビュー(コモンコントロール)を元に作成されています。したがって、ListView.Handleを使えばWin32 APIのリストビューと同様の手順でソートマークやスプリットボタンを表示できます。

手順はおおよそ以下の通りです。なお、ドロップダウン(スプリットボタンのクリック)などに応答するためにはウィンドウプロシージャーの処理が必要となりますが、ここでは対応していません。

  1. ListViewを作成してコンテナへ追加する。
    • ※追加は以下の操作後でも可能ですが、スプリットボタンのみ先に追加しないとリセットされるようです。
  2. SendMessageLVM_GETHEADERでヘッダーのウィンドウハンドルを取得する。
  3. SendMessageHDM_GETITEMW/HDM_SETITEMWでヘッダーのアイテムを操作する。このときHD_ITEMW構造体にHDI_FORMATを使用して操作対象をヘッダーのフォーマットに限定する。

実際のソースコードは以下の通りです。上記の操作を'ListViewHeaderUtility.cs'のListViewHeaderUtility静的クラスにまとめています。プロジェクトはC# .NET 5.0のWindowsアプリケーションとして作成してください。

Program.cs

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

        // フォームとリストビューの作成
        var form = new Form();
        var listView1 = new ListView();
        listView1.View = View.Details;
        listView1.Dock = DockStyle.Fill;
        listView1.Columns.Add("Column0");
        listView1.Columns.Add("Column1");
        listView1.Columns.Add("Column2");
        listView1.Columns.Add("Column3");
        listView1.Columns.Add("Column4");
        listView1.Columns.Add("Column5");
        // 最初のカラムだけソートマークの切り替え対応
        listView1.ColumnClick += (sender, e) =>
        {
            if (ListViewHeaderUtility.GetSortDown(listView1, 0))
            {
                ListViewHeaderUtility.SetSortDown(listView1, 0, false);
                ListViewHeaderUtility.SetSortUp(listView1, 0, true);
            }
            else
            {
                ListViewHeaderUtility.SetSortDown(listView1, 0, true);
                ListViewHeaderUtility.SetSortUp(listView1, 0, false);
            }
        };
        form.Controls.Add(listView1);

        // ヘッダーフォーマットの変更
        // ソートマークやスプリットボタンの追加
        ListViewHeaderUtility.SetSortDown(listView1, 0, true);
        ListViewHeaderUtility.SetSortUp(listView1, 1, true);
        ListViewHeaderUtility.SetSortUp(listView1, 2, true);
        ListViewHeaderUtility.SetSortDown(listView1, 2, true);
        ListViewHeaderUtility.SetSplitButton(listView1, 3, true);
        ListViewHeaderUtility.SetSplitButton(listView1, 4, true);
        ListViewHeaderUtility.SetSortDown(listView1, 4, true);
        ListViewHeaderUtility.SetSplitButton(listView1, 5, true);
        ListViewHeaderUtility.SetSortUp(listView1, 5, true);

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

        Application.Run(form);
    }
}

ListViewHeaderUtility.cs

using System.Runtime.InteropServices;
using System.Runtime.Versioning;

[SupportedOSPlatform("Windows")]
static class ListViewHeaderUtility
{
    public static IntPtr GetHeaderWindowHandle(ListView listView)
    {
        if (listView == null)
            throw new ArgumentNullException(nameof(listView));

        return NativeMethods.SendMessage(listView.Handle, Constants.LVM_GETHEADER, 0, 0);
    }

    public static int GetFormat(ListView listView, int column)
    {
        if (listView == null)
            throw new ArgumentNullException(nameof(listView));
        if (!(0 <= column && column < listView.Columns.Count))
            throw new ArgumentOutOfRangeException(nameof(column));

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

    public static void SetFormat(ListView listView, int column, int format)
    {
        if (listView == null)
            throw new ArgumentNullException(nameof(listView));
        if (!(0 <= column && column < listView.Columns.Count))
            throw new ArgumentOutOfRangeException(nameof(column));

        HD_ITEMW item = default;
        item.mask = Constants.HDI_FORMAT;
        item.fmt = format;
        var ret = NativeMethods.SendMessage(
            GetHeaderWindowHandle(listView),
            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 static bool GetSortUp(ListView listView, int column)
        => HasFlag(GetFormat(listView, column), Constants.HDF_SORTUP);

    public static void SetSortUp(ListView listView, int column, bool flag)
        => SetFormat(listView, column,
            SetFlags(GetFormat(listView, column), Constants.HDF_SORTUP, flag));

    public static bool GetSortDown(ListView listView, int column)
        => HasFlag(GetFormat(listView, column), Constants.HDF_SORTDOWN);

    public static void SetSortDown(ListView listView, int column, bool flag)
        => SetFormat(listView, column,
            SetFlags(GetFormat(listView, column), Constants.HDF_SORTDOWN, flag));

    public static bool GetFixedWidth(ListView listView, int column)
        => HasFlag(GetFormat(listView, column), Constants.HDF_FIXEDWIDTH);

    public static void SetFixedWidth(ListView listView, int column, bool flag)
        => SetFormat(listView, column,
            SetFlags(GetFormat(listView, column), Constants.HDF_FIXEDWIDTH, flag));

    public static bool GetSplitButton(ListView listView, int column)
        => HasFlag(GetFormat(listView, column), Constants.HDF_SPLITBUTTON);

    public static void SetSplitButton(ListView listView, int column, bool flag)
        => SetFormat(listView, column,
            SetFlags(GetFormat(listView, column), Constants.HDF_SPLITBUTTON, flag));

    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_GETITEMW = HDM_FIRST + 11;
        public const int HDM_SETITEMW = HDM_FIRST + 12;

        public const uint HDI_FORMAT = 0x0004;

        // Windows XP以降
        public const int HDF_SORTUP = 0x0400;
        public const int HDF_SORTDOWN = 0x0200;
        // Windows Vista以降
        public const int HDF_FIXEDWIDTH = 0x0100;
        public const int HDF_SPLITBUTTON = 0x1000000;
    }

    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);
    }

    [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;
    }
}

参考